diff --git a/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.i18n.yaml index 71c61113a6..d8e3f3fcaf 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.i18n.yaml @@ -9,8 +9,8 @@ en: 2eeb916eb65c66e7 zh: 7bf5c0abb8bb61e4 /agent-note-deepseek-account-models-come-from-the-account-listing-and-gate-by-client-version/decision: - en: 42e800c4cd3b6ebe - zh: 06fabe5a9681fa27 + en: 0125e33ba966c8ac + zh: bf6af389336999af /agent-note-deepseek-account-models-come-from-the-account-listing-and-gate-by-client-version/alternatives-considered: en: 7c7880fdb0cc95f6 zh: 2b8a539192626789 diff --git a/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.md b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.md index 75265db1ce..867198fba2 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.md +++ b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.md @@ -12,8 +12,8 @@ The account route's catalog was deployment configuration: a build advertised the - [`llm-deepseek-account`](../../../../packages/llm/llm-deepseek-account/README.md) loads the listing lazily for each credential, waits for it once up to `modelsTimeoutMs`, refreshes it in the background after `modelsMaxAgeMs`, keeps the last good listing when a load fails or returns no model, and serves the bundled catalog before any listing loads. The cache lives in memory and is keyed by endpoint and token fingerprint, so per-account rollouts stay separate. - A listed model is available when its `client_requirements["deepseek-harness"].min_version` is at most the current DeepSeek Harness version in semantic-version order. Without that declaration, only ids the built-in catalog knows are available; the others stay hidden, because a client cannot tell which future version supports them. A request for a refused listed model fails before network I/O with `CLIENT_UPGRADE_REQUIRED` or `MODEL_UNSUPPORTED`. -- Listed capabilities replace bundled ones field by field; the bundled entry keeps client-side knowledge such as tool-update mode and image budgets; `modelOverrides` applies last and reuses the `models` entry fields and validation. `max_output_tokens` bounds the request default instead of becoming it. -- The shared transport gains only two provider hooks, `connect` and `admit`; listing, gating, and merging stay in the account package. +- Listed capabilities replace bundled ones field by field; the bundled entry keeps client-side knowledge the listing does not declare, such as image budgets and a protocol mode the listing omits or sets to `null`; `modelOverrides` applies last and reuses the `models` entry fields and validation. `max_output_tokens` bounds the request default instead of becoming it. +- The shared transport gains only two provider hooks, `connect` and `admit`. Listing, gating, and merging live in the shared transport, which the [official route](2026-10-22-deepseek-official-model-listing.md) also uses. ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.zh.md b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.zh.md index 7199091c73..37487fbc88 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.zh.md @@ -12,8 +12,8 @@ Status: implemented - [`llm-deepseek-account`](../../../../packages/llm/llm-deepseek-account/README.zh.md) 按凭据懒加载列表,首次访问最多等待 `modelsTimeoutMs`,超过 `modelsMaxAgeMs` 后在后台刷新;获取失败或返回的列表中没有模型时保留上一份,还没有加载到任何列表时使用内置目录。缓存只在内存中,以端点和 token 指纹为键,因此按账号灰度互不干扰。 - 列出的模型在其 `client_requirements["deepseek-harness"].min_version` 按 semver 顺序不高于当前 DeepSeek Harness 版本时可用。没有这个声明时,只有内置目录认识的 id 可用,其余保持隐藏,因为客户端无法判断将来哪个版本会支持它们。请求被拒绝的已列出模型时,在网络请求之前以 `CLIENT_UPGRADE_REQUIRED` 或 `MODEL_UNSUPPORTED` 失败。 -- 列表中的能力逐字段覆盖内置能力;内置条目保留工具更新模式、图片预算等客户端知识;`modelOverrides` 最后叠加,复用 `models` 条目的字段与校验。`max_output_tokens` 只限制请求默认值,不直接成为默认值。 -- 共享传输层只新增 `connect` 与 `admit` 两个提供方钩子;列表、门控与合并都留在账号包中。 +- 列表中的能力逐字段覆盖内置能力;内置条目保留列表没有声明的客户端知识,例如图片预算,以及列表没有给出或设为 `null` 的协议模式;`modelOverrides` 最后叠加,复用 `models` 条目的字段与校验。`max_output_tokens` 只限制请求默认值,不直接成为默认值。 +- 共享传输层只新增 `connect` 与 `admit` 两个提供方钩子。列表、门控与合并位于共享传输包中,[官方路由](2026-10-22-deepseek-official-model-listing.zh.md)也使用它们。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/architecture/2026-10-22-deepseek-official-model-listing.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-22-deepseek-official-model-listing.i18n.yaml new file mode 100644 index 0000000000..cc59fcf7ad --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-22-deepseek-official-model-listing.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-10-22-deepseek-official-model-listing.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-22-deepseek-official-model-listing.md +/agent-note-the-official-deepseek-route-follows-the-official-model-listing-until-its-catalog-is-customized: + en: 20de6c2a1bc643b4 + zh: 2fe21450cebf7f32 +/agent-note-the-official-deepseek-route-follows-the-official-model-listing-until-its-catalog-is-customized/problem: + en: 51a513e6134e6ad9 + zh: e0b19412c9b88e24 +/agent-note-the-official-deepseek-route-follows-the-official-model-listing-until-its-catalog-is-customized/decision: + en: ceb2f960085577c3 + zh: 1bbc833b7cf24070 +/agent-note-the-official-deepseek-route-follows-the-official-model-listing-until-its-catalog-is-customized/alternatives-considered: + en: fa126e4f4e0790b4 + zh: c4f6d266d1ba6753 +/agent-note-the-official-deepseek-route-follows-the-official-model-listing-until-its-catalog-is-customized/consequences: + en: 047e30a0f55acb24 + zh: 804e15532d172157 diff --git a/.agents/notes/implemented/architecture/2026-10-22-deepseek-official-model-listing.md b/.agents/notes/implemented/architecture/2026-10-22-deepseek-official-model-listing.md new file mode 100644 index 0000000000..341cc43776 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-22-deepseek-official-model-listing.md @@ -0,0 +1,36 @@ +# Agent Note: The official DeepSeek route follows the official model listing until its catalog is customized + +Status: implemented + +English | [中文](2026-10-22-deepseek-official-model-listing.zh.md) + +## Problem + +The `deepseek-official` route offered whatever its `models` setting held, and that setting defaults to the catalog bundled with each build. A model the DeepSeek API added reached API-key users only through a release, while the account route already followed the service's `GET /models` listing at the same origin. Unlike the account route's catalog, the official route's catalog is user-editable: the Models page stores an edited catalog in `models`, and a gateway behind `baseURL` declares its models there. + +## Decision + +- [`llm-deepseek-api-key`](../../../../packages/llm/llm-deepseek-api-key/README.md) follows `GET https://api.deepseek.com/models`, authenticated with the API key in `x-api-key`, only while the Messages origin is `https://api.deepseek.com` and `models` equals the built-in catalog. Any other endpoint or catalog sends no listing request. +- Loading, refresh, fallback, merging, and the client-version gate are the [account route's](2026-09-24-deepseek-account-remote-model-catalog.md): a first access waits up to `modelsTimeoutMs`, a listing older than `modelsMaxAgeMs` refreshes in the background on the next access, a failure keeps the last good listing or the built-in catalog, and the cache lives in memory, keyed by endpoint and key fingerprint. +- A customized `models` list wins whole: it replaces the catalog and stops listing requests. A list is customized when it resolves differently from the built-in catalog, because the schema default materializes an absent `models` as the built-in list and the Models page shows that default as its inherited rows. +- Switching between a customized catalog and the listing invalidates the cache from a `loader/volatile-update` listener, so Restore defaults reloads the listing on the next access even when the cached listing is fresh. +- The listing request, its cache, catalog resolution, and the gate live in the shared [Messages transport](../../../../packages/llm/llm-deepseek/README.md) because two routes consume them; the transport's provider hooks remain `connect` and `admit`. + +## Alternatives considered + +**Follow the listing on every endpoint.** A gateway that publishes an OpenAI-style `/models` returns entries without `api_capabilities.anthropic_messages`, which the decoder hides, so the route would offer no models. Every such gateway would also receive an extra request carrying the key. + +**Merge the customized catalog with the listing by model id.** Customized users would keep receiving new listed models, but a model the user deleted would return, and the Models page cannot hide one listed model. + +**Mark an unset `models` by removing its schema default.** An absent value would select the listing without comparing catalogs, but the Models page would lose the inherited rows it shows and copies, with their hidden `systemPromptUpdate` and `toolUpdate` fields, into a customized catalog. Rows copied from the generic model-discovery API would drop those fields. + +**Fetch once per process, or refresh on a timer.** A single fetch leaves a long-running Web or desktop host on its startup catalog, and one transient failure pins it to the built-in catalog. A timer sends requests while the route is idle. Access-triggered background refresh matches the account route. + +**Reuse the cached listing after Restore defaults.** The cache could serve a listing loaded before the customization for up to `modelsMaxAgeMs`; the request is cheap, so restoring always reloads it. The reload starts on the next access rather than inside the listener, which keeps credential lookup and its failures on the request path; the Web page's settings refresh makes that access right after the save. + +## Consequences + +- API-key users on the official endpoint receive listed models without a release, and an older build hides or refuses a model that needs a newer client, as account users' builds do. +- A customized catalog receives no later listed models until Restore defaults, and a `models` list identical to the built-in catalog counts as uncustomized. The comparison uses the running version's built-in catalog, so a saved list equal to an earlier version's built-in catalog counts as customized once the built-in catalog changes. +- The Models page shows the built-in rows while the listing is followed, so a model only the listing offers appears only in model selectors, and editing the page's catalog drops it. +- The first catalog access on the official endpoint can wait up to `modelsTimeoutMs`, and a suite that boots the route there must answer the listing itself to stay off the network. diff --git a/.agents/notes/implemented/architecture/2026-10-22-deepseek-official-model-listing.zh.md b/.agents/notes/implemented/architecture/2026-10-22-deepseek-official-model-listing.zh.md new file mode 100644 index 0000000000..ea6073171a --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-22-deepseek-official-model-listing.zh.md @@ -0,0 +1,36 @@ +# Agent Note: DeepSeek 官方路由在目录未自定义时跟随官方模型列表 + +Status: implemented + +[English](2026-10-22-deepseek-official-model-listing.md) | 中文 + +## 问题 + +`deepseek-official` 路由提供的是 `models` 设置里的内容,而这个设置默认是每个构建自带的目录。DeepSeek API 新增的模型只能随发版到达 API key 用户,而账号路由已经在同一个 origin 上跟随服务端的 `GET /models` 列表。与账号路由不同,官方路由的目录可以由用户编辑:模型页把编辑后的目录存进 `models`,`baseURL` 后面的网关也在这里声明自己的模型。 + +## 决策 + +- [`llm-deepseek-api-key`](../../../../packages/llm/llm-deepseek-api-key/README.zh.md) 只在 Messages 的 origin 是 `https://api.deepseek.com`、且 `models` 等于内置目录时,跟随 `GET https://api.deepseek.com/models`,并用 `x-api-key` 携带 API key 鉴权。其他端点或目录不发列表请求。 +- 加载、刷新、兜底、合并与客户端版本门控沿用[账号路由](2026-09-24-deepseek-account-remote-model-catalog.zh.md)的规则:首次访问最多等待 `modelsTimeoutMs`,列表超过 `modelsMaxAgeMs` 后在下一次访问时后台刷新,失败时保留上一份成功的列表或使用内置目录,缓存只在内存中,以端点和 key 指纹为键。 +- 自定义的 `models` 列表整份优先:它替换目录,并停止列表请求。解析结果与内置目录不同的列表才算自定义,因为 schema 默认值会把缺省的 `models` 物化为内置列表,模型页也把这个默认值显示为继承的行。 +- 在自定义目录与列表之间切换时,一个 `loader/volatile-update` 监听器会让缓存失效,因此点恢复默认后,即使缓存的列表尚未过期,下一次访问也会重新加载。 +- 列表请求、缓存、目录解析和门控放在共享的 [Messages 传输包](../../../../packages/llm/llm-deepseek/README.zh.md)中,因为现在有两条路由使用它们;传输层的提供方钩子仍然只有 `connect` 和 `admit`。 + +## 考虑过的替代方案 + +**在所有端点上跟随列表。** 发布 OpenAI 风格 `/models` 的网关返回的条目没有 `api_capabilities.anthropic_messages`,解码器会隐藏它们,路由就不提供任何模型。每个这样的网关还会多收到一个携带 key 的请求。 + +**按模型 id 合并自定义目录与列表。** 自定义用户会继续收到新列出的模型,但用户删掉的模型会重新出现,而模型页无法隐藏某一个列出的模型。 + +**去掉 `models` 的 schema 默认值来标记未设置。** 缺省值不用比较目录就能选中列表,但模型页会失去它显示的继承行;这些行连同隐藏的 `systemPromptUpdate`、`toolUpdate` 字段会被复制进自定义目录。从通用模型发现 API 复制的行会丢掉这些字段。 + +**每个进程只获取一次,或按定时器刷新。** 只获取一次会让常驻的 Web 或桌面宿主停留在启动时的目录,一次偶发失败就会让它固定在内置目录。定时器会在路由空闲时发请求。由访问触发的后台刷新与账号路由一致。 + +**恢复默认后沿用缓存的列表。** 缓存可能在 `modelsMaxAgeMs` 内一直提供自定义之前加载的列表;列表请求代价很小,所以恢复默认总是重新加载。重新加载从下一次访问开始,而不是在监听器里进行,这样凭据查询及其失败都留在请求路径上;Web 页面保存后的设置刷新会立即发起这次访问。 + +## 影响 + +- 使用官方端点的 API key 用户无需发版就能看到列出的模型,旧构建会像账号用户的构建一样隐藏或拒绝需要新版客户端的模型。 +- 自定义目录在恢复默认之前收不到之后列出的模型;与内置目录完全相同的 `models` 列表视为未自定义。比较的是当前版本的内置目录,所以保存的列表若等于旧版本的内置目录,内置目录变化后就算作自定义。 +- 跟随列表期间,模型页仍显示内置的行,因此只有列表提供的模型只出现在模型选择器里,在模型页编辑目录会丢掉它。 +- 官方端点上的第一次目录访问可能等待最多 `modelsTimeoutMs`,在该端点上启动路由的测试套件必须自己应答列表请求,才能不访问网络。 diff --git a/.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.i18n.yaml b/.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.i18n.yaml index 4abceb8733..2bc76443c3 100644 --- a/.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.i18n.yaml @@ -9,8 +9,8 @@ en: 329c6ff947559b11 zh: 5c4c1053236d319b /agent-note-plugin-market-discover-page-and-index-reader/decision: - en: cc4d17a51dd479b7 - zh: 1fecf669b117f51c + en: dd32bedaa763a4cb + zh: 27b1a81bd744d8c1 /agent-note-plugin-market-discover-page-and-index-reader/alternatives-considered: en: 62e7691dc8efd5c0 zh: b13769634979b950 diff --git a/.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.md b/.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.md index 505ea944e6..95724fa8ec 100644 --- a/.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.md +++ b/.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.md @@ -12,7 +12,7 @@ The plugin manager owns profile package operations and must not remount while an ## Decision -The feature ships as the experimental optional [custom plugin markets bundle](../../../../packages/experimental/plugin-market-bundle/README.md), off by default, which mounts both rows below; with it off, neither the service, the Discover tab, nor the `plugin_market` tool exists, and the Plugins page shows no tabs. A separate Host service, [`pluginMarket`](../../../../packages/boot/plugin-market/README.md), reads indexes. It requires `pluginManager` for read-only calls and never writes the profile. Its `indexes` configuration field is volatile: Settings writes only volatile fields, and the Loader commits a volatile-only change into the running reference without remounting any plugin. Other fields (`fetchTimeoutMs`, `maxBytes`, `cacheTtlMs`) are ordinary configuration. +The feature ships as the experimental optional [custom plugin indexes bundle](../../../../packages/experimental/plugin-market-bundle/README.md), off by default, which mounts both rows below; with it off, neither the service, the Discover tab, nor the `plugin_market` tool exists, and the Plugins page shows no tabs. A separate Host service, [`pluginMarket`](../../../../packages/boot/plugin-market/README.md), reads indexes. It requires `pluginManager` for read-only calls and never writes the profile. Its `indexes` configuration field is volatile: Settings writes only volatile fields, and the Loader commits a volatile-only change into the running reference without remounting any plugin. Other fields (`fetchTimeoutMs`, `maxBytes`, `cacheTtlMs`) are ordinary configuration. Index URLs use `http:` or `https:`, without credentials or fragment. `file:` is excluded because relative icon references would resolve to URLs a browser cannot load. `search` and `entry` accept only a configured URL, so the Host never fetches an address a client supplies. The Host fetches through the process proxy, refuses redirects, bounds the body, and validates with the shared format package; an invalid index affects only its own URL. diff --git a/.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.zh.md b/.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.zh.md index f924fb0641..136df83518 100644 --- a/.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.zh.md +++ b/.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.zh.md @@ -12,7 +12,7 @@ Status: implemented ## 决策 -该功能以实验性的可选[自定义插件市场组合包](../../../../packages/experimental/plugin-market-bundle/README.zh.md)发布,默认关闭,并挂载下面两行;关闭时,服务、「发现」标签页和 `plugin_market` 工具都不存在,插件页也不显示标签页。由独立的 Host 服务 [`pluginMarket`](../../../../packages/boot/plugin-market/README.zh.md) 读取索引。它依赖 `pluginManager` 进行只读调用,从不写入 profile。它的 `indexes` 配置字段是 volatile 字段:Settings 只写入 volatile 字段,Loader 把只涉及 volatile 字段的变更提交到运行中的引用,不重新挂载任何插件。其他字段(`fetchTimeoutMs`、`maxBytes`、`cacheTtlMs`)是普通配置。 +该功能以实验性的可选[自定义插件索引组合包](../../../../packages/experimental/plugin-market-bundle/README.zh.md)发布,默认关闭,并挂载下面两行;关闭时,服务、「发现」标签页和 `plugin_market` 工具都不存在,插件页也不显示标签页。由独立的 Host 服务 [`pluginMarket`](../../../../packages/boot/plugin-market/README.zh.md) 读取索引。它依赖 `pluginManager` 进行只读调用,从不写入 profile。它的 `indexes` 配置字段是 volatile 字段:Settings 只写入 volatile 字段,Loader 把只涉及 volatile 字段的变更提交到运行中的引用,不重新挂载任何插件。其他字段(`fetchTimeoutMs`、`maxBytes`、`cacheTtlMs`)是普通配置。 索引 URL 使用 `http:` 或 `https:`,不含凭据或片段。排除 `file:`,因为相对图标引用会解析为浏览器无法加载的 URL。`search` 和 `entry` 只接受已配置的 URL,因此 Host 从不获取客户端提供的地址。Host 通过进程代理获取,拒绝重定向,限制响应体大小,并用共享的格式包校验;无效索引只影响它自己的 URL。 diff --git a/apps/cli/tests/built-bin.e2e.ts b/apps/cli/tests/built-bin.e2e.ts index 7a80f1c760..a97d158697 100644 --- a/apps/cli/tests/built-bin.e2e.ts +++ b/apps/cli/tests/built-bin.e2e.ts @@ -443,6 +443,9 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', }, SPAWN_TIMEOUT_MS + 30_000) it('serves the SDK protocol with an absolute-path overlay plugin and exits after shutdown', async () => { + // On the official endpoint the route requests its model listing with this key; + // the loopback mock keeps the run offline and records any model call. + const server = await startMockLlmServer({ sequence: ['success'] }) const home = mkdtempSync(join(tmpdir(), 'dsh-built-sdk-')) const pluginPath = join(home, 'plugin #100%.mjs') const marker = join(home, 'plugin-loaded') @@ -465,6 +468,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', DSH_HOME: home, DSH_TELEMETRY_DISABLED: '1', DEEPSEEK_API_KEY: 'built-sdk-profile-no-call', + DEEPSEEK_BASE_URL: server.baseURL, }, extendEnv: false, }) @@ -504,10 +508,12 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', expect(result.signal, stderr).toBeUndefined() expect(result.exitCode, stderr).toBe(0) expect(stderr).toBe('') + expect(server.requests).toEqual([]) expect(readFileSync(marker, 'utf8')).toBe('loaded') } finally { child.kill('SIGKILL') await child + await server.close() rmSync(home, { recursive: true, force: true }) } }, SPAWN_TIMEOUT_MS + 30_000) diff --git a/apps/cli/tests/profiles/headless/tests/expected/goal-tools/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/goal-tools/stream-json.expected.jsonl index ab06b02706..c54f84df0b 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/goal-tools/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/goal-tools/stream-json.expected.jsonl @@ -4,7 +4,7 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Probe strict-schema fillers against missing-goal revision 1, then create a durable goal to finish the snapshot proof and inspect it."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Probe strict-schema fillers against miss","messageSeqs":[8],"source":{"kind":"fallback"}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl index 6ea0d43a17..e747f996ad 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl @@ -4,7 +4,7 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"say pong"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"say pong","messageSeqs":[8],"source":{"kind":"fallback"}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl index f24d221b96..bc7c728199 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl @@ -4,7 +4,7 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"say pong"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"say pong","messageSeqs":[8],"source":{"kind":"fallback"}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/provider-retry/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/provider-retry/stream-json.expected.jsonl index a667224c5f..5792ae3774 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/provider-retry/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/provider-retry/stream-json.expected.jsonl @@ -4,7 +4,7 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"retry the transient provider failure"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"retry the transient provider failure","messageSeqs":[8],"source":{"kind":"fallback"}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/semantic-checkpoint/session.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/semantic-checkpoint/session.expected.jsonl index 2afe21d157..11ae2ec827 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/semantic-checkpoint/session.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/semantic-checkpoint/session.expected.jsonl @@ -18,7 +18,7 @@ {"type":"step/start","data":{"turn":2,"step":1}} {"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}} {"type":"user/message","data":{"content":[{"type":"text","text":"Continue safely from the interrupted operation."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Perform one side-effecting remote mutati","messageSeqs":[3],"source":{"kind":"fallback"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl index 5ea5179b2e..9da1c6379a 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl @@ -16,7 +16,7 @@ {"type":"step/start","data":{"turn":2,"step":1}} {"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}} {"type":"user/message","data":{"content":[{"type":"text","text":"Call list_agents once and report what it shows."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Start a background job.","messageSeqs":[3],"source":{"kind":"fallback"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl index 3b3a9e5c7b..3f3dee9c95 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl @@ -16,7 +16,7 @@ {"type":"step/start","data":{"turn":2,"step":1}} {"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}} {"type":"user/message","data":{"content":[{"type":"text","text":"Delegate the write probe to a subagent."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":["bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"resume","startsSeries":true}} {"type":"developer/message","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"get_goal"},{"type":"tool-addition","toolName":"glob"},{"type":"tool-addition","toolName":"grep"},{"type":"tool-addition","toolName":"interrupt_agent"},{"type":"tool-addition","toolName":"job_kill"},{"type":"tool-addition","toolName":"job_list"},{"type":"tool-addition","toolName":"job_output"},{"type":"tool-addition","toolName":"list_agents"},{"type":"tool-addition","toolName":"read"},{"type":"tool-addition","toolName":"read_image"},{"type":"tool-addition","toolName":"send_message"},{"type":"tool-addition","toolName":"skill"},{"type":"tool-addition","toolName":"subagent"},{"type":"tool-addition","toolName":"subagent_fork"},{"type":"tool-addition","toolName":"todo_write"},{"type":"tool-addition","toolName":"update_goal"},{"type":"tool-addition","toolName":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"working_directory"},{"type":"tool-addition","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":18},"surfaceOp":"append"} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl index f06a75700f..441cab1aa2 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl @@ -4,7 +4,7 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Start one continuable subagent and acknowledge its completion notice. Do not call list_agents or send_message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Start one continuable subagent and","messageSeqs":[8],"source":{"kind":"fallback"}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/offline-edit/session.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/offline-edit/session.expected.jsonl index 5ed8464fb7..cbb82cb603 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/offline-edit/session.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/offline-edit/session.expected.jsonl @@ -17,7 +17,7 @@ {"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}} {"type":"user/message","data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"\nUpdated instructions from: AGENTS.md\n\nThis file changed after it was loaded. Use the following content instead of the previously loaded instructions from this file.\n\nNew workspace instruction after offline edit.\n\n"}],"source":{"kind":"agent-instructions","form":"instructions","changes":[{"action":"replace","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"d8375b516f158718bd3463bc8eb7ed42c011b29f"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Remember the workspace instruction.","messageSeqs":[3],"source":{"kind":"fallback"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/precedence-change/session.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/precedence-change/session.expected.jsonl index f52d5f06e9..3bd9c55f95 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/precedence-change/session.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/precedence-change/session.expected.jsonl @@ -17,7 +17,7 @@ {"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}} {"type":"user/message","data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"\nThis complete workspace instruction baseline replaces all earlier workspace instruction baselines. The following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nCurrent AGENTS rule.\n\n\nInstructions from: CLAUDE.md\n\nCurrent CLAUDE rule.\n\n"}],"source":{"kind":"agent-instructions","form":"instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".git\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"AGENTS.md\",\"CLAUDE.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"7f53d2327837129750aef117f9754a001c46cf68"},{"action":"set","scope":".\u0000CLAUDE.md","path":"CLAUDE.md","digest":"5b1e9e3fd759eee6b43ceff899e47fb10c64701a"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Remember the workspace instruction.","messageSeqs":[3],"source":{"kind":"fallback"}}} diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index 0185ef8505..e1b34a0dca 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -33,8 +33,8 @@ en: 372297005b57a08b zh: 074a635ac17e4362 /deepseek-harness-desktop/installation-ownership: - en: 7cbb1f9e5006bd34 - zh: 01e9e15a75a90450 + en: 160b500cdcd6a39a + zh: 6c267f1762ec3689 /deepseek-harness-desktop/installation-ownership/runtime-and-plugin-activation: en: 4951cd3ee659eb33 zh: 96bdd6358dad587a @@ -45,8 +45,8 @@ en: faf7676ba18bbf2f zh: f1d7b75ad348c548 /deepseek-harness-desktop/develop/startup-onboarding: - en: d87d51b901b72ecd - zh: 525da1c4d56af659 + en: 67dc7ef3e293236a + zh: 1b224db684a2d1d7 /deepseek-harness-desktop/develop/welcome-window-appearance: en: 617a89aeb28131b9 zh: 4d84bd5d86501c86 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index f3280ec254..57da4db0f3 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -155,7 +155,7 @@ The product UI retains Web actions, including "Open In..." through the shared au Update copy preserves the complete version, including prerelease suffixes, without adding v or V. When no update is available, the dialog title reports the result and the body shows the current version. -Electron chooses typed English, Chinese, or Japanese shell copy from its application locale and falls back to English. The macOS application bundle declares English, Simplified Chinese, and Japanese in `CFBundleLocalizations`, allowing macOS to match the initial application language to the user’s preferred languages. Saved Client UI language preferences still take precedence for the main interface. Windows NSIS installer resources live in one JSON file per language under `installer/locales/`; packaging generates NSIS strings and discovers the installer language list from these files. English, Simplified Chinese, and Japanese resources must have matching keys and format placeholders; `pnpm run check:i18n` validates them. The native installer test builds and exercises each configured language. Platform account pages and onboarding artwork use English for languages other than Chinese. On Windows, the main document's language updates desktop menus, recovery and update prompts. The repository Client UI i18n gate checks desktop sources. +Electron chooses typed shell copy in the eight [shipped Client languages](../../packages/client/locale/README.md) from its application locale and falls back to English. The macOS application bundle declares all eight languages in `CFBundleLocalizations`, allowing macOS to match the initial application language to the user’s preferred languages. Saved Client UI language preferences still take precedence for the main interface. Windows NSIS installer resources live in one JSON file per language under `installer/locales/`; packaging generates NSIS strings and discovers the installer language list from these files. All shipped language resources must have matching keys, format placeholders, and command examples; `pnpm run check:i18n` validates them. The native installer test builds and exercises each configured language. Platform account pages and onboarding artwork use English for languages other than Chinese. On Windows, the main document's language updates desktop menus, recovery and update prompts. The repository Client UI i18n gate checks desktop sources. Windows and macOS relay native fullscreen transitions and the state after each page load to the renderer, so overlays can adapt their caption clearance. @@ -217,7 +217,7 @@ Repeated launches and `dsh://open` keep the workspace hidden until the startup c Desktop checks configured model API-key references after the Host starts and before opening the workspace. With no configured key, the welcome window offers the API-key page. Save and continue writes through the existing credential service using the official DeepSeek provider's configured reference, then opens the workspace. Set up later opens the workspace without saving the draft or a completion setting; the next process launch checks credentials again. Back to sign in returns to the entry and clears the unsaved key and validation message. Buttons keep their labels and block competing actions while saving or opening the workspace. The Desktop preload marker suppresses the Web credential dialog, while retaining the Models settings page and the welcome notice. -The welcome window reads the shared `locale.preference` before it appears. An explicit English, Chinese, or Japanese choice wins; otherwise Desktop picks the first supported OS language and falls back to English. The main UI reads the same preference and OS language order through its isolated preload before mounting. Language changes in Settings update the shell’s current dictionary and menu; automatic selection writes no preference. The welcome window has no language selector. +The welcome window reads the shared `locale.preference` before it appears. An explicit supported language choice wins; otherwise Desktop picks the first supported OS language and falls back to English. The main UI reads the same preference and OS language order through its isolated preload before mounting. Language changes in Settings update the shell’s current dictionary and menu; automatic selection writes no preference. The welcome window has no language selector. The browser-login waiting page offers a copy-link action for the current pending authorization, a loading indicator, and cancellation. Clipboard failures leave the copy action available for retry. Copy feedback resets after two seconds; the copied state disables the link until it resets. Welcome text uses Montserrat Light with system fallbacks; large action buttons keep the system font, while text buttons use Montserrat Light. English introductory copy is 24px throughout. Chinese introductory copy is 24px with a 26px product name. Login action buttons are 240px wide with 14px labels. Authorization status headings use 20px Montserrat Regular text. The API-key page uses a 20px heading and a 14px back action, with 84px between the secondary button’s bottom edge and the window bottom. diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index 9afca70398..d9760964bb 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -155,7 +155,7 @@ Electron 拥有 `$DSH_HOME/profiles/desktop`。其 `dependencies` 包含 pnpm 升级文案完整展示版本号(包括预发布后缀),不额外添加 v 或 V。无可用更新时,弹窗标题显示检查结果,正文显示当前版本。 -Electron 根据应用语言选择类型化的英文、中文或日文 shell 文案,并回退到英文。macOS 应用包通过 `CFBundleLocalizations` 声明支持英语、简体中文和日语,让 macOS 根据用户的首选语言匹配初始应用语言。主界面仍优先使用已保存的 Client UI 语言偏好。 Windows NSIS 安装器在 `installer/locales/` 下按语言保存独立 JSON 资源;打包从这些文件生成 NSIS 文案和安装器语言清单。英文、简体中文和日语资源的键与格式占位符必须一致,`pnpm run check:i18n` 负责检查。原生安装器测试逐个构建并验证配置中的语言。账户平台页面和引导插图对中文以外的语言使用英文。在 Windows 上,主文档的语言会更新桌面菜单、恢复与更新提示。仓库 Client UI i18n 检查覆盖桌面端源码。 +Electron 根据应用语言选择类型化的八种[内置 Client 语言](../../packages/client/locale/README.zh.md)的 shell 文案,并回退到英文。macOS 应用包通过 `CFBundleLocalizations` 声明支持全部八种语言,让 macOS 根据用户的首选语言匹配初始应用语言。主界面仍优先使用已保存的 Client UI 语言偏好。Windows NSIS 安装器在 `installer/locales/` 下按语言保存独立 JSON 资源;打包从这些文件生成 NSIS 文案和安装器语言清单。全部内置语言资源的键、格式占位符和命令示例必须一致,`pnpm run check:i18n` 负责检查。原生安装器测试逐个构建并验证配置中的语言。账户平台页面和引导插图对中文以外的语言使用英文。在 Windows 上,主文档的语言会更新桌面菜单、恢复与更新提示。仓库 Client UI i18n 检查覆盖桌面端源码。 Windows 和 macOS 会在原生全屏切换及每次页面加载后向渲染器同步全屏状态,让浮层调整顶栏避让距离。 @@ -217,7 +217,7 @@ API Key 输入框初始为空,并通过 `autocomplete="new-password"` 请求 C Desktop 在 Host 启动后、打开工作区前检查模型 API Key 引用是否已配置。没有已配置的密钥时,欢迎窗口提供 API Key 页面。“保存并继续”通过现有凭证服务写入 DeepSeek 官方提供方配置的引用,然后打开工作区。“稍后配置”打开工作区,但不保存草稿或完成标记;下次进程启动时会重新检查凭证。“返回登录”回到入口并清空未保存的密钥和校验提示。保存或打开工作区期间,按钮保持原文案并禁用竞争操作。Desktop preload 标记使 Web 凭证弹窗不再显示,同时保留模型设置页和欢迎须知。 -欢迎窗口在显示前读取共享的 `locale.preference`。用户明确选择的英文、中文或日文优先;否则 Desktop 按系统语言顺序匹配支持的语言,并以英文兜底。主界面在挂载前通过隔离 preload 读取同一偏好和系统语言顺序。在设置中切换语言会更新桌面壳的当前词典和菜单;自动选择不会写入偏好。欢迎窗口不提供语言切换入口。 +欢迎窗口在显示前读取共享的 `locale.preference`。用户明确选择的受支持语言优先;否则 Desktop 按系统语言顺序匹配支持的语言,并以英文兜底。主界面在挂载前通过隔离 preload 读取同一偏好和系统语言顺序。在设置中切换语言会更新桌面壳的当前词典和菜单;自动选择不会写入偏好。欢迎窗口不提供语言切换入口。 等待浏览器登录时,欢迎页提供当前待授权请求的链接复制入口、加载指示和取消操作;剪贴板写入失败后可以重试复制,复制结果提示在两秒后恢复;已复制状态下链接禁用,恢复后可再次点击。Welcome 文字使用 Montserrat Light 并回退到系统字体,底部大按钮保留系统字体,文字按钮使用 Montserrat Light。英文欢迎正文及产品名均为 24px,中文欢迎正文为 24px、产品名为 26px。登录操作按钮宽 240px,文字为 14px。授权状态标题使用 20px Montserrat Regular 字重。API Key 页的标题为 20px,返回操作为 14px,次级按钮底边距窗口底部 84px。 diff --git a/apps/desktop/installer/locales/de_DE.json b/apps/desktop/installer/locales/de_DE.json new file mode 100644 index 0000000000..faf725b88e --- /dev/null +++ b/apps/desktop/installer/locales/de_DE.json @@ -0,0 +1,40 @@ +{ + "nsisLanguage": "GERMAN", + "strings": { + "INSTALLER_INSTALL": "Jetzt installieren", + "INSTALLER_FINISH": "Fertigstellen", + "INSTALLER_NATIVE_PROGRESS": "Installation wird vorbereitet… %d%%", + "INSTALLER_PROGRESS_EXTRACT": "Dateien extrahieren… %d%%", + "INSTALLER_PROGRESS_COPY": "Dateien werden installiert… %d%%", + "INSTALLER_PROGRESS_REGISTER": "Abschluss der Installation… %d%%", + "INSTALLER_PROGRESS_CLEAN": "Bereinigen temporärer Dateien… %d%%", + "INSTALLER_CHOOSE_PATH": "Standort wählen", + "INSTALLER_BROWSE": "Durchsuchen", + "INSTALLER_LAUNCH": "Jetzt starten", + "INSTALLER_LAUNCH_FAILED": "DeepSeek Harness konnte nicht gestartet werden. Versuchen Sie es noch einmal oder deaktivieren Sie „Jetzt starten“ und schließen Sie das Installationsprogramm.", + "INSTALLER_UI_ERROR": "Das Installationsprogramm konnte sein Fenster nicht initialisieren. Schließen Sie es und versuchen Sie es erneut.", + "INSTALLER_THEME_ERROR": "Verwenden Sie /THEME=auto, /THEME=light oder /THEME=dark.", + "INSTALLER_FAILED": "Die Installation wurde nicht abgeschlossen. Schließen Sie dieses Installationsprogramm und versuchen Sie es erneut. Wenn das Problem weiterhin besteht, laden Sie das Installationsprogramm erneut herunter.", + "INSTALLER_RUNNING": "DeepSeek Harness läuft. Beenden Sie es über die Taskleiste und versuchen Sie es erneut.", + "INSTALLER_DIRECTORY_FAILED": "Das Installationsverzeichnis konnte nicht ersetzt werden.", + "INSTALLER_DIRECTORY_HINT": "Überprüfen Sie den Zugriff auf das Installationsverzeichnis und schließen Sie Programme mithilfe seiner Dateien. Führen Sie dann das Installationsprogramm erneut aus. Kopieren Sie die Fehlerdetails, wenn das Problem weiterhin besteht.", + "INSTALLER_DIRECTORY_RESTORE_HINT": "Die vorherige Installation konnte nicht wiederhergestellt werden. Behalten Sie das in den Fehlerdetails angezeigte Sicherungsverzeichnis bei und wenden Sie sich vor der erneuten Installation an den Support.", + "INSTALLER_PATH_INVALID": "Wählen Sie einen vollständigen lokalen Ordnerpfad. Laufwerksstämme, Systemordner, Links und Sonderzeichen werden nicht unterstützt.", + "INSTALLER_PATH_OWNERSHIP": "Wählen Sie einen leeren Ordner oder den registrierten DeepSeek Harness-Installationsordner.", + "INSTALLER_PATH_WRITABLE": "Dieser Ort ist nicht beschreibbar. Wählen Sie einen Ordner, der für den aktuellen Benutzer verfügbar ist.", + "INSTALLER_DISK_SPACE": "Es ist nicht genügend freier Speicherplatz vorhanden. Wählen Sie einen anderen Standort.", + "INSTALLER_PER_USER": "Dieses Installationsprogramm unterstützt nur den aktuellen Benutzer. Deinstallieren Sie zunächst die vorhandene All-User-Installation.", + "INSTALLER_EXTRACT_FAILED": "Die Anwendungsdateien konnten nicht extrahiert werden.", + "INSTALLER_EXTRACT_HINT": "Möglicherweise hat die Sicherheitssoftware eine Datei blockiert oder die Festplatte ist voll. Schließen Sie dieses Installationsprogramm und versuchen Sie es erneut. Wenn es weiterhin fehlschlägt, senden Sie uns die Fehlerdetails unten.", + "INSTALLER_EXTRACT_COPY": "Fehlerdetails kopieren", + "INSTALLER_EXTRACT_EXPAND": "Details anzeigen", + "INSTALLER_EXTRACT_COLLAPSE": "Details ausblenden", + "INSTALLER_EXTRACT_SAVED": "Der vollständige Bericht wurde unter %s gespeichert", + "INSTALLER_EXTRACT_UNSAVED": "Der vollständige Bericht konnte nicht gespeichert werden. Verwenden Sie „Fehlerdetails kopieren“, um sie beizubehalten.", + "INSTALLER_EXTRACT_COPIED": "Fehlerdetails wurden in die Zwischenablage kopiert." + }, + "uninstaller": { + "welcome": "Willkommen", + "finished": "entfernt worden" + } +} diff --git a/apps/desktop/installer/locales/es_ES.json b/apps/desktop/installer/locales/es_ES.json new file mode 100644 index 0000000000..d4fbfd972f --- /dev/null +++ b/apps/desktop/installer/locales/es_ES.json @@ -0,0 +1,40 @@ +{ + "nsisLanguage": "SPANISHINTERNATIONAL", + "strings": { + "INSTALLER_INSTALL": "Instalar ahora", + "INSTALLER_FINISH": "Finalizar", + "INSTALLER_NATIVE_PROGRESS": "Preparando la instalación… %d%%", + "INSTALLER_PROGRESS_EXTRACT": "Extrayendo archivos… %d%%", + "INSTALLER_PROGRESS_COPY": "Instalando archivos… %d%%", + "INSTALLER_PROGRESS_REGISTER": "Terminando la instalación… %d%%", + "INSTALLER_PROGRESS_CLEAN": "Limpieza de archivos temporales… %d%%", + "INSTALLER_CHOOSE_PATH": "Elige ubicación", + "INSTALLER_BROWSE": "Examinar", + "INSTALLER_LAUNCH": "Lanzar ahora", + "INSTALLER_LAUNCH_FAILED": "No se pudo iniciar DeepSeek Harness. Vuelva a intentarlo o desmarque Iniciar ahora y cierre el instalador.", + "INSTALLER_UI_ERROR": "El instalador no pudo inicializar su ventana. Ciérralo y vuelve a intentarlo.", + "INSTALLER_THEME_ERROR": "Utilice /THEME=auto, /THEME=light o /THEME=dark.", + "INSTALLER_FAILED": "La instalación no terminó. Cierre este instalador y vuelva a intentarlo. Si el problema persiste, descargue el instalador nuevamente.", + "INSTALLER_RUNNING": "DeepSeek Harness se está ejecutando. Salga de la bandeja del sistema y vuelva a intentarlo.", + "INSTALLER_DIRECTORY_FAILED": "No se pudo reemplazar el directorio de instalación.", + "INSTALLER_DIRECTORY_HINT": "Verifique el acceso al directorio de instalación y cierre los programas usando sus archivos, luego ejecute el instalador nuevamente. Copie los detalles del error si el problema persiste.", + "INSTALLER_DIRECTORY_RESTORE_HINT": "No se pudo restaurar la instalación anterior. Mantenga el directorio de respaldo que se muestra en los detalles del error y comuníquese con el soporte antes de volver a instalar.", + "INSTALLER_PATH_INVALID": "Elija una ruta de carpeta local completa. No se admiten raíces de unidades, carpetas del sistema, enlaces ni caracteres especiales.", + "INSTALLER_PATH_OWNERSHIP": "Elija una carpeta vacía o la carpeta de instalación registrada de DeepSeek Harness.", + "INSTALLER_PATH_WRITABLE": "Esta ubicación no se puede escribir. Elija una carpeta disponible para el usuario actual.", + "INSTALLER_DISK_SPACE": "No hay suficiente espacio libre en disco. Elija otra ubicación.", + "INSTALLER_PER_USER": "Este instalador solo admite el usuario actual. Primero desinstale la instalación existente para todos los usuarios.", + "INSTALLER_EXTRACT_FAILED": "No se pudieron extraer los archivos de la aplicación.", + "INSTALLER_EXTRACT_HINT": "Es posible que el software de seguridad haya bloqueado un archivo o que el disco esté lleno. Cierra este instalador y vuelve a intentarlo. Si sigue fallando, envíenos los detalles del error a continuación.", + "INSTALLER_EXTRACT_COPY": "Copiar detalles del error", + "INSTALLER_EXTRACT_EXPAND": "Mostrar detalles", + "INSTALLER_EXTRACT_COLLAPSE": "Ocultar detalles", + "INSTALLER_EXTRACT_SAVED": "El informe completo se guardó en %s.", + "INSTALLER_EXTRACT_UNSAVED": "No se pudo guardar el informe completo. Utilice Copiar detalles del error para conservarlo.", + "INSTALLER_EXTRACT_COPIED": "Los detalles del error se copiaron al portapapeles." + }, + "uninstaller": { + "welcome": "Bienvenido", + "finished": "ha sido desinstalado" + } +} diff --git a/apps/desktop/installer/locales/fr_FR.json b/apps/desktop/installer/locales/fr_FR.json new file mode 100644 index 0000000000..326ead62a8 --- /dev/null +++ b/apps/desktop/installer/locales/fr_FR.json @@ -0,0 +1,40 @@ +{ + "nsisLanguage": "FRENCH", + "strings": { + "INSTALLER_INSTALL": "Installer maintenant", + "INSTALLER_FINISH": "Terminer", + "INSTALLER_NATIVE_PROGRESS": "Préparation de l'installation… %d%%", + "INSTALLER_PROGRESS_EXTRACT": "Extraction de fichiers… %d%%", + "INSTALLER_PROGRESS_COPY": "Installation des fichiers… %d%%", + "INSTALLER_PROGRESS_REGISTER": "Finition de l'installation… %d%%", + "INSTALLER_PROGRESS_CLEAN": "Nettoyage des fichiers temporaires… %d%%", + "INSTALLER_CHOOSE_PATH": "Choisissez un emplacement", + "INSTALLER_BROWSE": "Parcourir", + "INSTALLER_LAUNCH": "Lancez-vous maintenant", + "INSTALLER_LAUNCH_FAILED": "DeepSeek Harness n'a pas pu être lancé. Réessayez ou désactivez Lancer maintenant et fermez le programme d'installation.", + "INSTALLER_UI_ERROR": "Le programme d'installation n'a pas pu initialiser sa fenêtre. Fermez-le et réessayez.", + "INSTALLER_THEME_ERROR": "Utilisez /THEME=auto, /THEME=light ou /THEME=dark.", + "INSTALLER_FAILED": "L'installation n'est pas terminée. Fermez ce programme d'installation et réessayez. Si le problème persiste, téléchargez à nouveau le programme d'installation.", + "INSTALLER_RUNNING": "DeepSeek Harness est en cours d'exécution. Quittez-le de la barre d'état système et réessayez.", + "INSTALLER_DIRECTORY_FAILED": "Le répertoire d'installation n'a pas pu être remplacé.", + "INSTALLER_DIRECTORY_HINT": "Vérifiez l'accès au répertoire d'installation et fermez les programmes à l'aide de ses fichiers, puis réexécutez le programme d'installation. Copiez les détails de l'erreur si le problème persiste.", + "INSTALLER_DIRECTORY_RESTORE_HINT": "L'installation précédente n'a pas pu être restaurée. Conservez le répertoire de sauvegarde indiqué dans les détails de l'erreur et contactez l'assistance avant de procéder à une nouvelle installation.", + "INSTALLER_PATH_INVALID": "Choisissez un chemin de dossier local complet. Les racines du lecteur, les dossiers système, les liens et les caractères spéciaux ne sont pas pris en charge.", + "INSTALLER_PATH_OWNERSHIP": "Choisissez un dossier vide ou le dossier d'installation DeepSeek Harness enregistré.", + "INSTALLER_PATH_WRITABLE": "Cet emplacement n'est pas accessible en écriture. Choisissez un dossier disponible pour l'utilisateur actuel.", + "INSTALLER_DISK_SPACE": "Il n'y a pas assez d'espace disque libre. Choisissez un autre emplacement.", + "INSTALLER_PER_USER": "Ce programme d'installation prend en charge uniquement l'utilisateur actuel. Désinstallez d’abord l’installation existante pour tous les utilisateurs.", + "INSTALLER_EXTRACT_FAILED": "Les fichiers de l'application n'ont pas pu être extraits.", + "INSTALLER_EXTRACT_HINT": "Un logiciel de sécurité a peut-être bloqué un fichier ou le disque est peut-être plein. Fermez ce programme d'installation et réessayez. Si l'échec persiste, envoyez-nous les détails de l'erreur ci-dessous.", + "INSTALLER_EXTRACT_COPY": "Copier les détails de l'erreur", + "INSTALLER_EXTRACT_EXPAND": "Afficher les détails", + "INSTALLER_EXTRACT_COLLAPSE": "Masquer les détails", + "INSTALLER_EXTRACT_SAVED": "Le rapport complet a été enregistré dans %s", + "INSTALLER_EXTRACT_UNSAVED": "Le rapport complet n'a pas pu être enregistré. Utilisez Copier les détails de l'erreur pour le conserver.", + "INSTALLER_EXTRACT_COPIED": "Les détails de l'erreur ont été copiés dans le presse-papiers." + }, + "uninstaller": { + "welcome": "Bienvenue", + "finished": "a été supprimé" + } +} diff --git a/apps/desktop/installer/locales/pt_BR.json b/apps/desktop/installer/locales/pt_BR.json new file mode 100644 index 0000000000..dda249e46f --- /dev/null +++ b/apps/desktop/installer/locales/pt_BR.json @@ -0,0 +1,40 @@ +{ + "nsisLanguage": "PORTUGUESEBR", + "strings": { + "INSTALLER_INSTALL": "Instale agora", + "INSTALLER_FINISH": "Concluir", + "INSTALLER_NATIVE_PROGRESS": "Preparando a instalação… %d%%", + "INSTALLER_PROGRESS_EXTRACT": "Extraindo arquivos… %d%%", + "INSTALLER_PROGRESS_COPY": "Instalando arquivos… %d%%", + "INSTALLER_PROGRESS_REGISTER": "Concluindo a instalação… %d%%", + "INSTALLER_PROGRESS_CLEAN": "Limpando arquivos temporários… %d%%", + "INSTALLER_CHOOSE_PATH": "Escolha o local", + "INSTALLER_BROWSE": "Procurar", + "INSTALLER_LAUNCH": "Lançar agora", + "INSTALLER_LAUNCH_FAILED": "DeepSeek Harness não pôde ser iniciado. Tente novamente ou desmarque Iniciar agora e feche o instalador.", + "INSTALLER_UI_ERROR": "O instalador não conseguiu inicializar sua janela. Feche-o e tente novamente.", + "INSTALLER_THEME_ERROR": "Use /THEME=auto, /THEME=light ou /THEME=dark.", + "INSTALLER_FAILED": "A instalação não foi concluída. Feche este instalador e tente novamente. Se o problema persistir, baixe o instalador novamente.", + "INSTALLER_RUNNING": "DeepSeek Harness está em execução. Saia da bandeja do sistema e tente novamente.", + "INSTALLER_DIRECTORY_FAILED": "O diretório de instalação não pôde ser substituído.", + "INSTALLER_DIRECTORY_HINT": "Verifique o acesso ao diretório de instalação e feche os programas usando seus arquivos e execute o instalador novamente. Copie os detalhes do erro se o problema persistir.", + "INSTALLER_DIRECTORY_RESTORE_HINT": "A instalação anterior não pôde ser restaurada. Mantenha o diretório de backup mostrado nos detalhes do erro e entre em contato com o suporte antes de instalar novamente.", + "INSTALLER_PATH_INVALID": "Escolha um caminho completo para a pasta local. Raízes de unidade, pastas de sistema, links e caracteres especiais não são suportados.", + "INSTALLER_PATH_OWNERSHIP": "Escolha uma pasta vazia ou a pasta de instalação DeepSeek Harness registrada.", + "INSTALLER_PATH_WRITABLE": "Este local não é gravável. Escolha uma pasta disponível para o usuário atual.", + "INSTALLER_DISK_SPACE": "Não há espaço livre suficiente em disco. Escolha outro local.", + "INSTALLER_PER_USER": "Este instalador oferece suporte apenas ao usuário atual. Desinstale primeiro a instalação existente para todos os usuários.", + "INSTALLER_EXTRACT_FAILED": "Os arquivos do aplicativo não puderam ser extraídos.", + "INSTALLER_EXTRACT_HINT": "O software de segurança pode ter bloqueado um arquivo ou o disco pode estar cheio. Feche este instalador e tente novamente. Se continuar falhando, envie-nos os detalhes do erro abaixo.", + "INSTALLER_EXTRACT_COPY": "Copiar detalhes do erro", + "INSTALLER_EXTRACT_EXPAND": "Mostrar detalhes", + "INSTALLER_EXTRACT_COLLAPSE": "Ocultar detalhes", + "INSTALLER_EXTRACT_SAVED": "O relatório completo foi salvo em %s", + "INSTALLER_EXTRACT_UNSAVED": "Não foi possível salvar o relatório completo. Use Copiar detalhes do erro para mantê-lo.", + "INSTALLER_EXTRACT_COPIED": "Os detalhes do erro foram copiados para a área de transferência." + }, + "uninstaller": { + "welcome": "Bem-vindo", + "finished": "foi desinstalado" + } +} diff --git a/apps/desktop/installer/locales/ru_RU.json b/apps/desktop/installer/locales/ru_RU.json new file mode 100644 index 0000000000..e5fec3f8d1 --- /dev/null +++ b/apps/desktop/installer/locales/ru_RU.json @@ -0,0 +1,40 @@ +{ + "nsisLanguage": "RUSSIAN", + "strings": { + "INSTALLER_INSTALL": "Установить сейчас", + "INSTALLER_FINISH": "Готово", + "INSTALLER_NATIVE_PROGRESS": "Подготовка установки… %d%%", + "INSTALLER_PROGRESS_EXTRACT": "Извлечение файлов… %d%%", + "INSTALLER_PROGRESS_COPY": "Установка файлов… %d%%", + "INSTALLER_PROGRESS_REGISTER": "Завершение установки… %d%%", + "INSTALLER_PROGRESS_CLEAN": "Очистка временных файлов… %d%%", + "INSTALLER_CHOOSE_PATH": "Выберите местоположение", + "INSTALLER_BROWSE": "Обзор", + "INSTALLER_LAUNCH": "Запустить сейчас", + "INSTALLER_LAUNCH_FAILED": "DeepSeek Harness не удалось запустить. Повторите попытку или снимите флажок «Запустить сейчас» и закройте программу установки.", + "INSTALLER_UI_ERROR": "Установщику не удалось инициализировать свое окно. Закройте его и попробуйте еще раз.", + "INSTALLER_THEME_ERROR": "Используйте /THEME=auto, /THEME=light или /THEME=dark.", + "INSTALLER_FAILED": "Установка не завершена. Закройте этот установщик и повторите попытку. Если проблема не устранена, загрузите установщик еще раз.", + "INSTALLER_RUNNING": "DeepSeek Harness работает. Закройте его из системного трея и повторите попытку.", + "INSTALLER_DIRECTORY_FAILED": "Не удалось заменить каталог установки.", + "INSTALLER_DIRECTORY_HINT": "Проверьте доступ к каталогу установки и закройте программы, использующие его файлы, затем снова запустите установщик. Скопируйте сведения об ошибке, если проблема не устранена.", + "INSTALLER_DIRECTORY_RESTORE_HINT": "Предыдущую установку восстановить не удалось. Сохраните каталог резервной копии, указанный в сведениях об ошибке, и обратитесь в службу поддержки перед повторной установкой.", + "INSTALLER_PATH_INVALID": "Выберите полный путь к локальной папке. Корни дисков, системные папки, ссылки и специальные символы не поддерживаются.", + "INSTALLER_PATH_OWNERSHIP": "Выберите пустую папку или зарегистрированную папку установки DeepSeek Harness.", + "INSTALLER_PATH_WRITABLE": "Это местоположение недоступно для записи. Выберите папку, доступную текущему пользователю.", + "INSTALLER_DISK_SPACE": "Недостаточно свободного места на диске. Выберите другое место.", + "INSTALLER_PER_USER": "Этот установщик поддерживает только текущего пользователя. Сначала удалите существующую установку для всех пользователей.", + "INSTALLER_EXTRACT_FAILED": "Файлы приложения не удалось извлечь.", + "INSTALLER_EXTRACT_HINT": "Возможно, программное обеспечение безопасности заблокировало файл или диск переполнен. Закройте этот установщик и повторите попытку. Если ошибка продолжает возникать, отправьте нам сведения об ошибке ниже.", + "INSTALLER_EXTRACT_COPY": "Скопировать сведения об ошибке", + "INSTALLER_EXTRACT_EXPAND": "Показать детали", + "INSTALLER_EXTRACT_COLLAPSE": "Скрыть детали", + "INSTALLER_EXTRACT_SAVED": "Полный отчет сохранен в %s.", + "INSTALLER_EXTRACT_UNSAVED": "Полный отчет не удалось сохранить. Используйте Скопировать сведения об ошибке, чтобы сохранить их.", + "INSTALLER_EXTRACT_COPIED": "Подробности об ошибке скопированы в буфер обмена." + }, + "uninstaller": { + "welcome": "Вас приветствует мастер удаления", + "finished": "удалена" + } +} diff --git a/apps/desktop/scripts/electron-builder-config.mjs b/apps/desktop/scripts/electron-builder-config.mjs index 09530b12a4..6cf6754801 100644 --- a/apps/desktop/scripts/electron-builder-config.mjs +++ b/apps/desktop/scripts/electron-builder-config.mjs @@ -166,7 +166,7 @@ export function createElectronBuilderConfig( category: 'public.app-category.developer-tools', // macOS matches the application locale against this bundle, not Electron Framework resources. extendInfo: { - CFBundleLocalizations: ['en', 'zh_CN', 'ja'], + CFBundleLocalizations: ['en', 'zh_CN', 'ja', 'es', 'fr', 'de', 'ru', 'pt'], NSMicrophoneUsageDescription: 'DeepSeek Harness uses your microphone to transcribe speech into message drafts.', }, // Ad-hoc signing keeps the modified Electron executable runnable without a Developer ID. diff --git a/apps/desktop/scripts/installer-locales.mjs b/apps/desktop/scripts/installer-locales.mjs index f9f9b55138..5785a99021 100644 --- a/apps/desktop/scripts/installer-locales.mjs +++ b/apps/desktop/scripts/installer-locales.mjs @@ -10,7 +10,8 @@ const defaultDirectory = fileURLToPath(new URL('../installer/locales/', import.m * @returns {{id: string, nsisLanguage: string, strings: Record, uninstaller: Record}[]} Complete language resources. */ export function readInstallerLocales(directory = defaultDirectory) { - const locales = readdirSync(directory).filter(name => name.endsWith('.json')).sort().map(name => { + const locales = readdirSync(directory).filter(name => name.endsWith('.json')).sort((a, b) => + a === 'en_US.json' ? -1 : b === 'en_US.json' ? 1 : a.localeCompare(b)).map(name => { if (!/^[a-z]{2}_[A-Z]{2}\.json$/u.test(name)) throw new Error(`${name}: invalid installer language filename`) /** @type {unknown} */ const resource = JSON.parse(readFileSync(`${directory}/${name}`, 'utf8')) @@ -25,6 +26,7 @@ export function readInstallerLocales(directory = defaultDirectory) { const result = {} for (const [key, text] of Object.entries(value)) { if (typeof text !== 'string' || text.trim() === '') throw new Error(`${name}: ${key} must be nonempty text`) + if (/[\u200b\ufeff]/u.test(text)) throw new Error(`${name}: ${key} contains an invisible locale character`) result[key] = text } return result @@ -34,7 +36,7 @@ export function readInstallerLocales(directory = defaultDirectory) { return { id: name.slice(0, -5), nsisLanguage: resource.nsisLanguage, strings, uninstaller: dictionary('uninstaller' in resource ? resource.uninstaller : undefined) } }) - for (const id of ['en_US', 'zh_CN', 'ja_JP']) { + for (const id of ['en_US', 'zh_CN', 'ja_JP', 'es_ES', 'fr_FR', 'de_DE', 'ru_RU', 'pt_BR']) { if (!locales.some(locale => locale.id === id)) throw new Error(`installer locales: missing ${id}.json`) } const english = locales.find(locale => locale.id === 'en_US') @@ -49,8 +51,8 @@ export function readInstallerLocales(directory = defaultDirectory) { const actual = locale[field] if (Object.keys(expected).sort().join(',') !== Object.keys(actual).sort().join(',')) throw new Error(`${locale.id}: ${field} keys differ from English`) for (const [key, text] of Object.entries(expected)) { - const placeholders = (/** @type {string} */ value) => (value.match(/%.?/gu) ?? []).join(',') - if (placeholders(text) !== placeholders(actual[key])) throw new Error(`${locale.id}: ${key} changes format placeholders`) + const placeholders = (/** @type {string} */ value) => (value.match(/%.?|(? +/** Spanish dictionary, checked against the English keys. */ +export const es = { + cliCommandMenu: 'Administrar el comando dsh…', + cliCommandTitle: 'Administrar el comando dsh', + cliCommandLocation: 'Comando de escritorio: {path}', + cliCommandSelected: 'Comando dsh actual: {path}', + cliCommandTarget: 'Objetivo actual del lanzador: {path}', + cliCommandShadowed: 'Otro dsh tiene prioridad. Elimine o reordene esa instalación para usar el comando Escritorio de forma predeterminada. También puede ejecutar el comando Escritorio por su ruta completa.', + cliCommandSelectionUnknown: 'No se pudo verificar su comando de shell. Un alias u otra instalación de dsh puede tener prioridad.', + cliCommandInstalled: 'El comando Escritorio está instalado.', + cliCommandNotInstalled: 'Agregue el comando Escritorio a su terminal.', + cliCommandBroken: 'El comando de escritorio necesita reparación.', + cliCommandInstall: 'Instalar', + cliCommandRepair: 'Reparación', + cliCommandRemove: 'Quitar', + cliCommandClose: 'Cerrar', + cliCommandSwitch: '¿Continuar con el comando Escritorio?', + cliCommandPreserve: 'Se conservará un comando existente. Otras instalaciones y archivos de inicio del shell no se modificarán.', + cliCommandContinue: 'Continuar', + cliCommandInstallApp: 'Instale Desktop en su carpeta de Aplicaciones antes de administrar el comando dsh.', + cliCommandUpdating: 'Se está instalando una actualización. Administre el comando una vez finalizada la instalación.', + cliCommandNewTerminal: 'Abra una nueva terminal y ejecute dsh --version.', + cliCommandRemoved: 'Se eliminó el registro de comandos de escritorio.', + cliCommandPreviousRestored: 'El lanzador anterior ha sido restaurado.', + cliCommandOtherKept: 'Otras instalaciones de mando se han dejado en su lugar.', + cliCommandBackupKept: 'Se conserva un lanzador anterior en: {path}', + cliCommandChanged: 'El comando o RUTA cambió mientras este cuadro de diálogo estaba abierto. Abra Administrar comando dsh nuevamente para revisar el estado actual.', + cliCommandOwnershipError: 'El registro del comando o la copia de seguridad cambió. No se eliminó ningún comando no relacionado.', + cliCommandFailed: 'El comando no se pudo actualizar. Verifique que la aplicación esté instalada y que se pueda escribir en el destino, luego vuelva a intentarlo.', + application: 'Solicitud', + fileMenu: 'Archivo', + closePage: 'Cerrar página o ventana', + aboutMenu: 'Acerca de DeepSeek Harness', + aboutProduct: 'DeepSeek Harness', + aboutVersion: 'Versión V{version}', + hideApplication: 'Ocultar DeepSeek Harness', + hideOtherApplications: 'Ocultar otros', + showAllApplications: 'Mostrar todo', + quitApplication: 'Salir de DeepSeek Harness', + openApplication: 'Abrir DeepSeek Harness', + quit: 'Salir', + cancel: 'Cancelar', + quitTitle: '¿Salir de DeepSeek Harness?', + quitActiveTasks: 'Las tareas en ejecución se interrumpirán.', + quitScheduledTasks: 'Las tareas programadas no se ejecutarán mientras la aplicación esté cerrada.', + quitActiveAndScheduledTasks: 'Las tareas en ejecución se interrumpirán y las tareas programadas no se ejecutarán mientras la aplicación esté cerrada.', + backgroundNoticeBody: 'Las tareas en ejecución continuarán. Puede volver a abrir la ventana desde la bandeja del sistema.', + backgroundNoticeConfirm: 'Confirmar', + edit: 'Editar', + menuBar: 'Menú de aplicaciones', + delete: 'Eliminar', + undo: 'Deshacer', + redo: 'Rehacer', + cut: 'cortar', + copy: 'Copiar', + paste: 'Pegar', + selectAll: 'Seleccionar todo', + startupFailed: 'DeepSeek Harness no está disponible', + fatalSummary: 'La aplicación no pudo iniciarse o se detuvo inesperadamente.', + startupAddressInUse: 'Se está ejecutando otra instancia de DSH (como la web de dsh o la aplicación de escritorio). No pueden empezar al mismo tiempo. Salga de la otra instancia DSH en ejecución y luego reinicie.', + diagnosticTruncated: '… Detalles del error acortados.', + reportWrittenTo: 'Informe de diagnóstico: {path}', + startupReinstallAdvice: 'Si faltan archivos de la aplicación o están dañados, ciérrela y vuelva a instalarla. Sus tareas se almacenan por separado.', + exitApplication: 'Salir', + restartApplication: 'Reiniciar', + recoveryOperationFailed: 'La operación de recuperación falló', + disableThirdPartyPlugins: 'Deshabilite los complementos de terceros, haga una copia de seguridad del parche del perfil y reinicie', + welcomeTitle: 'DeepSeek Harness', + welcomeBrand: 'DeepSeek Harness', + welcomeTaglineBefore: 'Bienvenido a ', + welcomeTaglineBrand: 'DeepSeek Harness', + welcomeTaglineAfter: '', + welcomeDescription: 'Desarrollar potencial. Explora la inteligencia.', + welcomeAuthStarting: 'Abriendo inicio de sesión…', + welcomeAuthWaiting: '¿El navegador no se abrió automáticamente?', + welcomeAuthWaitingDescription: 'Copie el enlace de inicio de sesión y ábralo en su navegador para iniciar sesión.', + welcomeAuthExchanging: 'Completando el inicio de sesión…', + welcomeAuthExpired: 'Se agotó el tiempo de inicio de sesión', + welcomeAuthExpiredDescription: 'Inicia sesión nuevamente para continuar', + welcomeAuthFailed: 'No se pudo completar el inicio de sesión. Inténtelo de nuevo.', + welcomeAuthCopyLink: 'Copiar enlace de inicio de sesión', + welcomeAuthCopied: 'Copiado', + welcomeAuthCopyFailed: 'No se pudo copiar. Intentar otra vez.', + welcomeAuthCancel: 'Cancelar', + welcomeAuthRetry: 'Iniciar sesión nuevamente', + welcomeSignIn: 'Iniciar sesión', + welcomeApiKey: 'Agregar clave API', + welcomeKeyTitle: 'Agregue una clave API para comenzar', + welcomeKeyDescription: 'Configura los modelos oficiales DeepSeek para empezar a utilizar Harness', + welcomeKeyPlaceholder: 'Ingrese la clave API', + welcomeKeySave: 'Guardar y continuar', + welcomeKeyLater: 'Configurar más tarde', + welcomeKeyBack: 'Volver para iniciar sesión', + welcomeSessionExpired: 'Has cerrado sesión en tu cuenta, inicia sesión nuevamente.', + welcomeKeyBlank: 'Ingrese una clave API.', + welcomeKeyInvalid: 'Ingrese la clave API, sin comillas, espacios ni una asignación de variable de entorno.', + welcomeKeyFailed: 'No se pudo guardar la clave API. Por favor inténtalo de nuevo.', + welcomeContinueFailed: 'No se pudo abrir el espacio de trabajo. Por favor inténtalo de nuevo.', + checkUpdatesMenu: 'Buscar actualizaciones...', + reloadPageMenu: 'Recargar página', + restartAppHostMenu: 'Reiniciar la aplicación y el host', + updateCheckFailedTitle: 'Error en la comprobación de actualización', + updateCheckFailed: 'No se pudo buscar actualizaciones. Inténtelo de nuevo más tarde.', + updateDownloadFailed: 'No se pudo descargar la actualización. Por favor inténtalo de nuevo.', + updateInstallFailed: 'No se pudo instalar la actualización. Inténtelo de nuevo más tarde.', + updateCheckNetworkFailed: 'No se pudo buscar actualizaciones. Comprueba tu conexión y vuelve a intentarlo.', + updateDownloadNetworkFailed: 'No se pudo descargar la actualización. Comprueba tu conexión y vuelve a intentarlo.', + updateInstallNetworkFailed: 'No se pudo instalar la actualización. Comprueba tu conexión y vuelve a intentarlo.', + unknownError: 'Error desconocido', + updateCheckTitle: 'Buscar actualizaciones', + updateCurrentDetail: 'Versión actual: {version}', + updateCurrent: '¡Estás al día!', + updateChecking: 'Buscando actualizaciones...', + updateDownload: 'Descargar actualización', + updateDownloadedTitle: 'La versión {version} está lista para instalar.', + updateDownloadedDetail: 'La aplicación se cerrará durante la actualización y se volverá a abrir automáticamente cuando se complete.', + updateDownloadedDetailWindows: 'La aplicación se cerrará temporalmente durante la actualización y se volverá a abrir automáticamente cuando se complete.\n\nLa actualización puede tardar algún tiempo. Espere y no vuelva a iniciar la aplicación durante la instalación.', + updateClose: 'Cerrar', + updateAcknowledge: 'OK', + updateLater: 'Actualizar más tarde', + updateDownloading: 'Descargando {percent}%…', + updateVerifying: 'Verificando archivos de actualización...', + updateInstalling: 'Preparándose para reiniciar…', + updateRetry: 'Reintentar actualización', + updateActiveTasks: 'Las tareas aún están en progreso', + updateActiveTasksDetail: 'La actualización detendrá las tareas en curso y reiniciará la aplicación. ¿Continuar?', + updateStopTasks: 'Detener tareas y actualizar', + updateTasksChanged: 'Han comenzado nuevas tareas. Confirme nuevamente para detener las tareas y actualizar.', + updateTasksUnavailable: 'El estado de la tarea no está disponible. Intente actualizar nuevamente cuando el espacio de trabajo esté listo.', + updateFullscreenTimeout: 'No se pudo salir de la pantalla completa. Salga de la pantalla completa y luego vuelva a intentar la actualización.', + updateStopFailed: 'No se pudieron detener las tareas de forma segura. La actualización no se ha instalado. Inténtelo de nuevo más tarde.', + updateTechnicalDetails: 'Ver detalles técnicos', + updateTitle: 'Actualización DeepSeek Harness', + updateAvailable: 'Nueva versión disponible: {version}', + updateDetail: 'Una vez que se completa la descarga, puede instalar la actualización y reiniciar la aplicación.', + installAndRestart: 'Instalar y reiniciar', + later: 'Más tarde', + updateFailedTitle: 'Actualización fallida', + mandatoryTitle: 'Actualizar para continuar', + mandatoryDetail: 'Esta versión ya no es compatible. Actualiza para continuar. Las tareas en curso seguirán ejecutándose hasta que confirme la instalación y reinicie.', + mandatoryUnavailable: 'No se pudieron verificar los requisitos de actualización. Inténtelo de nuevo más tarde.', + policyLoginTitle: 'Inicie sesión en el entorno de prueba', + policyLoginRequired: 'Inicie sesión con Feishu para verificar los requisitos de actualización para esta versión de prueba. Iniciar sesión no descargará ni instalará una actualización.', + policyLogin: 'Iniciar sesión con Feishu', + policyLoginFailed: 'El inicio de sesión de Feishu no se completó. Por favor inténtalo de nuevo.', + policyLoginLoading: 'Cargando página de inicio de sesión...', + mandatoryNoRelease: 'No se encontró ninguna actualización compatible. Verifique nuevamente o comuníquese con el soporte.', + mandatoryRefresh: 'Comprobar de nuevo', + mandatoryPage: 'Descargar desde el sitio web oficial.', + mandatoryCopy: 'Copiar enlace de descarga', + mandatoryPageFailed: 'No se pudo abrir la página de descarga oficial. Copie el enlace y ábralo en su navegador.', + mandatoryViewDownload: 'Abrir página de descarga', + mandatoryViewPageFailed: 'No se pudo abrir la página de descarga. Por favor inténtalo de nuevo.', + mandatoryActionFailed: 'La acción fracasó. Por favor inténtalo de nuevo. Puede utilizar la aplicación nuevamente una vez completada la actualización.', + mandatoryReady: 'Actualización lista', + mandatoryVersion: 'Nueva versión: {version}', + mandatoryReadyDetail: 'La aplicación se cerrará durante la actualización y se volverá a abrir automáticamente cuando se complete.', + mandatoryDeferred: 'Las tareas en curso seguirán ejecutándose. Complete la actualización antes de volver a utilizar la aplicación.', + mandatoryContinue: 'Continuar actualizando', + mandatoryInspecting: 'Comprobando tareas...', + mandatoryStopping: 'Deteniendo tareas…', + mandatoryRestarting: 'La aplicación se reiniciará en breve. Espere por favor.', + mandatoryDownloadFailed: 'Los archivos de actualización no se pudieron descargar ni preparar. Vuelva a intentarlo.', + mandatoryInstallFailed: 'La actualización no se ha instalado. Verifique las tareas nuevamente y vuelva a intentarlo.', + mandatoryOpenHelp: '¿La página no se abrió?', + mandatoryReopen: 'Abre la página de descarga oficial nuevamente.', + mandatoryCopied: 'Enlace copiado', + mandatoryCopyFailed: 'No se pudo copiar el enlace. Selecciónelo y cópielo a continuación.', + mandatoryAddress: 'Enlace de descarga', + mandatoryNotification: 'Regrese a la aplicación para confirmar la instalación y reiniciar.', +} satisfies DesktopMessages + +/** French dictionary, checked against the English keys. */ +export const fr = { + cliCommandMenu: 'Gérer la commande dsh…', + cliCommandTitle: 'Gérer la commande dsh', + cliCommandLocation: 'Commande de bureau : {path}', + cliCommandSelected: 'Commande dsh actuelle : {path}', + cliCommandTarget: 'Cible actuelle du lanceur : {path}', + cliCommandShadowed: 'Un autre dsh est prioritaire. Supprimez ou réorganisez cette installation pour utiliser la commande Bureau par défaut. Vous pouvez également exécuter la commande Desktop par son chemin complet.', + cliCommandSelectionUnknown: "Votre commande shell n'a pas pu être vérifiée. Un alias ou une autre installation dsh peut être prioritaire.", + cliCommandInstalled: 'La commande Bureau est installée.', + cliCommandNotInstalled: 'Ajoutez la commande Bureau à votre terminal.', + cliCommandBroken: 'La commande Bureau doit être réparée.', + cliCommandInstall: 'Installer', + cliCommandRepair: 'Réparer', + cliCommandRemove: 'Supprimer', + cliCommandClose: 'Fermer', + cliCommandSwitch: 'Continuer avec la commande Bureau ?', + cliCommandPreserve: 'Une commande existante sera conservée. Les autres installations et fichiers de démarrage du shell ne seront pas modifiés.', + cliCommandContinue: 'Continuer', + cliCommandInstallApp: 'Installez Desktop dans votre dossier Applications avant de gérer la commande dsh.', + cliCommandUpdating: "Une mise à jour est en cours d'installation. Gérez la commande une fois l'installation terminée.", + cliCommandNewTerminal: 'Ouvrez un nouveau terminal et exécutez dsh --version.', + cliCommandRemoved: 'Enregistrement des commandes de bureau supprimé.', + cliCommandPreviousRestored: 'Le lanceur précédent a été restauré.', + cliCommandOtherKept: "D'autres installations de commandement ont été laissées en place.", + cliCommandBackupKept: 'Un lanceur précédent est conservé à : {path}', + cliCommandChanged: "La commande ou PATH a changé pendant que cette boîte de dialogue était ouverte. Ouvrez à nouveau la commande Gérer dsh pour vérifier l'état actuel.", + cliCommandOwnershipError: "L'enregistrement ou la sauvegarde de la commande a été modifiée. Aucune commande sans rapport n'a été supprimée.", + cliCommandFailed: "La commande n'a pas pu être mise à jour. Vérifiez que l'application est installée et que la destination est accessible en écriture, puis réessayez.", + application: 'Application', + fileMenu: 'Fichier', + closePage: 'Fermer la page ou la fenêtre', + aboutMenu: 'À propos de DeepSeek Harness', + aboutProduct: 'DeepSeek Harness', + aboutVersion: 'VersionV{version}', + hideApplication: 'Masquer DeepSeek Harness', + hideOtherApplications: 'Masquer les autres', + showAllApplications: 'Afficher tout', + quitApplication: 'Quitter DeepSeek Harness', + openApplication: 'Ouvrir DeepSeek Harness', + quit: 'Quitter', + cancel: 'Annuler', + quitTitle: 'Quitter DeepSeek Harness ?', + quitActiveTasks: 'Les tâches en cours seront interrompues.', + quitScheduledTasks: "Les tâches planifiées ne s'exécuteront pas lorsque l'application est fermée.", + quitActiveAndScheduledTasks: "Les tâches en cours d'exécution seront interrompues et les tâches planifiées ne s'exécuteront pas tant que l'application sera fermée.", + backgroundNoticeBody: "Les tâches en cours continueront. Vous pouvez rouvrir la fenêtre à partir de la barre d'état système.", + backgroundNoticeConfirm: 'Confirmer', + edit: 'Modifier', + menuBar: 'Menu des applications', + delete: 'Supprimer', + undo: 'Annuler', + redo: 'Refaire', + cut: 'Couper', + copy: 'Copier', + paste: 'Coller', + selectAll: 'Sélectionner tout', + startupFailed: "DeepSeek Harness n'est pas disponible", + fatalSummary: "L'application n'a pas pu démarrer ou s'est arrêtée de manière inattendue.", + startupAddressInUse: "Une autre instance DSH (telle que le Web dsh ou l'application de bureau) est en cours d'exécution. Ils ne peuvent pas démarrer en même temps. Quittez l'autre instance DSH en cours d'exécution, puis redémarrez.", + diagnosticTruncated: '… Détails de l’erreur raccourcis.', + reportWrittenTo: 'Rapport de diagnostic : {path}', + startupReinstallAdvice: "Si les fichiers de l'application sont manquants ou endommagés, fermez l'application et réinstallez-la. Vos tâches sont stockées séparément.", + exitApplication: 'Quitter', + restartApplication: 'Redémarrer', + recoveryOperationFailed: "L'opération de récupération a échoué", + disableThirdPartyPlugins: 'Désactivez les plugins tiers, sauvegardez le correctif de profil et redémarrez', + welcomeTitle: 'DeepSeek Harness', + welcomeBrand: 'DeepSeek Harness', + welcomeTaglineBefore: 'Bienvenue à ', + welcomeTaglineBrand: 'DeepSeek Harness', + welcomeTaglineAfter: '', + welcomeDescription: "Développer le potentiel. Explorez l'intelligence.", + welcomeAuthStarting: 'Ouverture de la connexion…', + welcomeAuthWaiting: "Le navigateur ne s'est pas ouvert automatiquement ?", + welcomeAuthWaitingDescription: 'Copiez le lien de connexion et ouvrez-le dans votre navigateur pour vous connecter.', + welcomeAuthExchanging: 'Connexion terminée…', + welcomeAuthExpired: 'La connexion a expiré', + welcomeAuthExpiredDescription: 'Connectez-vous à nouveau pour continuer', + welcomeAuthFailed: 'Impossible de terminer la connexion. Veuillez réessayer.', + welcomeAuthCopyLink: 'Copier le lien de connexion', + welcomeAuthCopied: 'Copié', + welcomeAuthCopyFailed: 'Impossible de copier. Essayer à nouveau.', + welcomeAuthCancel: 'Annuler', + welcomeAuthRetry: 'Connectez-vous à nouveau', + welcomeSignIn: 'Connectez-vous', + welcomeApiKey: 'Ajouter une clé API', + welcomeKeyTitle: 'Ajoutez une clé API pour commencer', + welcomeKeyDescription: 'Configurez les modèles DeepSeek officiels pour commencer à utiliser Harness', + welcomeKeyPlaceholder: 'Entrez la clé API', + welcomeKeySave: 'Enregistrer et continuer', + welcomeKeyLater: 'Configurer plus tard', + welcomeKeyBack: 'Retour pour vous connecter', + welcomeSessionExpired: 'Vous vous êtes déconnecté de votre compte, veuillez vous reconnecter.', + welcomeKeyBlank: 'Entrez une clé API.', + welcomeKeyInvalid: "Entrez la clé API elle-même, sans guillemets, espaces ni affectation de variable d'environnement.", + welcomeKeyFailed: "Impossible d'enregistrer la clé API. Veuillez réessayer.", + welcomeContinueFailed: "Impossible d'ouvrir l'espace de travail. Veuillez réessayer.", + checkUpdatesMenu: 'Vérifiez les mises à jour…', + reloadPageMenu: 'Recharger la page', + restartAppHostMenu: "Redémarrez l'application et l'hôte", + updateCheckFailedTitle: 'Échec de la vérification de la mise à jour', + updateCheckFailed: 'Impossible de vérifier les mises à jour. Veuillez réessayer plus tard.', + updateDownloadFailed: 'Impossible de télécharger la mise à jour. Veuillez réessayer.', + updateInstallFailed: "Impossible d'installer la mise à jour. Veuillez réessayer plus tard.", + updateCheckNetworkFailed: 'Impossible de vérifier les mises à jour. Vérifiez votre connexion et réessayez.', + updateDownloadNetworkFailed: 'Impossible de télécharger la mise à jour. Vérifiez votre connexion et réessayez.', + updateInstallNetworkFailed: "Impossible d'installer la mise à jour. Vérifiez votre connexion et réessayez.", + unknownError: 'Erreur inconnue', + updateCheckTitle: 'Vérifier les mises à jour', + updateCurrentDetail: 'Version actuelle : {version}', + updateCurrent: 'Vous êtes à jour !', + updateChecking: 'Vérification des mises à jour…', + updateDownload: 'Télécharger la mise à jour', + updateDownloadedTitle: 'La version {version} est prête à installer', + updateDownloadedDetail: "L'application se fermera pendant la mise à jour et rouvrira automatiquement une fois celle-ci terminée.", + updateDownloadedDetailWindows: "L'application se fermera temporairement pendant la mise à jour et rouvrira automatiquement une fois celle-ci terminée.\n\nLa mise à jour peut prendre un certain temps. Veuillez patienter et ne pas relancer l'application pendant l'installation.", + updateClose: 'Fermer', + updateAcknowledge: 'OK', + updateLater: 'Mettre à jour plus tard', + updateDownloading: 'Téléchargement de {percent}%…', + updateVerifying: 'Vérification des fichiers de mise à jour…', + updateInstalling: 'Préparation du redémarrage…', + updateRetry: 'Réessayez la mise à jour', + updateActiveTasks: 'Les tâches sont toujours en cours', + updateActiveTasksDetail: "La mise à jour arrêtera les tâches en cours et redémarrera l'application. Continuer?", + updateStopTasks: 'Arrêter les tâches et mettre à jour', + updateTasksChanged: 'De nouvelles tâches ont commencé. Confirmez à nouveau pour arrêter les tâches et mettre à jour.', + updateTasksUnavailable: "L'état de la tâche n'est pas disponible. Essayez à nouveau de mettre à jour lorsque l'espace de travail est prêt.", + updateFullscreenTimeout: 'Impossible de quitter le mode plein écran. Quittez le mode plein écran, puis réessayez la mise à jour.', + updateStopFailed: "Impossible d'arrêter les tâches en toute sécurité. La mise à jour n'a pas été installée. Veuillez réessayer plus tard.", + updateTechnicalDetails: 'Voir les détails techniques', + updateTitle: 'Mise à jour DeepSeek Harness', + updateAvailable: 'Nouvelle version disponible : {version}', + updateDetail: "Une fois le téléchargement terminé, vous pouvez installer la mise à jour et redémarrer l'application.", + installAndRestart: 'Installer et redémarrer', + later: 'Plus tard', + updateFailedTitle: 'Échec de la mise à jour', + mandatoryTitle: 'Mettre à jour pour continuer', + mandatoryDetail: "Cette version n'est plus prise en charge. Mettez à jour pour continuer. Les tâches en cours continueront à s'exécuter jusqu'à ce que vous confirmiez l'installation et redémarriez.", + mandatoryUnavailable: 'Impossible de vérifier les exigences de mise à jour. Veuillez réessayer plus tard.', + policyLoginTitle: "Connectez-vous à l'environnement de test", + policyLoginRequired: 'Connectez-vous avec Feishu pour vérifier les exigences de mise à jour pour cette version de test. La connexion ne téléchargera ni n’installera de mise à jour.', + policyLogin: 'Connectez-vous avec Feishu', + policyLoginFailed: 'La connexion à Feishu n’a pas abouti. Veuillez réessayer.', + policyLoginLoading: 'Chargement de la page de connexion…', + mandatoryNoRelease: "Aucune mise à jour compatible n'a été trouvée. Vérifiez à nouveau ou contactez l'assistance.", + mandatoryRefresh: 'Vérifiez à nouveau', + mandatoryPage: 'Télécharger depuis le site officiel', + mandatoryCopy: 'Copier le lien de téléchargement', + mandatoryPageFailed: "Impossible d'ouvrir la page de téléchargement officielle. Copiez le lien et ouvrez-le dans votre navigateur.", + mandatoryViewDownload: 'Ouvrir la page de téléchargement', + mandatoryViewPageFailed: "Impossible d'ouvrir la page de téléchargement. Veuillez réessayer.", + mandatoryActionFailed: "L'action a échoué. Veuillez réessayer. Vous pourrez réutiliser l'application une fois la mise à jour terminée.", + mandatoryReady: 'Mise à jour prête', + mandatoryVersion: 'Nouvelle version : {version}', + mandatoryReadyDetail: "L'application se fermera pendant la mise à jour et rouvrira automatiquement une fois celle-ci terminée.", + mandatoryDeferred: "Les tâches en cours continueront à s'exécuter. Terminez la mise à jour avant d'utiliser à nouveau l'application.", + mandatoryContinue: 'Continuer la mise à jour', + mandatoryInspecting: 'Vérification des tâches…', + mandatoryStopping: 'Arrêt des tâches…', + mandatoryRestarting: "L'application redémarrera sous peu. S'il vous plaît, attendez.", + mandatoryDownloadFailed: "Les fichiers de mise à jour n'ont pas pu être téléchargés ou préparés. Veuillez réessayer.", + mandatoryInstallFailed: "La mise à jour n'a pas été installée. Vérifiez à nouveau les tâches et réessayez.", + mandatoryOpenHelp: "La page ne s'est pas ouverte ?", + mandatoryReopen: 'Ouvrez à nouveau la page de téléchargement officielle', + mandatoryCopied: 'Lien copié', + mandatoryCopyFailed: 'Impossible de copier le lien. Sélectionnez-le et copiez-le ci-dessous.', + mandatoryAddress: 'Lien de téléchargement', + mandatoryNotification: "Revenez à l'application pour confirmer l'installation et redémarrer.", +} satisfies DesktopMessages + +/** German dictionary, checked against the English keys. */ +export const de = { + cliCommandMenu: 'dsh-Befehl verwalten…', + cliCommandTitle: 'dsh-Befehl verwalten', + cliCommandLocation: 'Desktop-Befehl: {path}', + cliCommandSelected: 'Aktueller dsh-Befehl: {path}', + cliCommandTarget: 'Aktuelles Launcher-Ziel: {path}', + cliCommandShadowed: 'Ein anderer dsh hat Vorrang. Entfernen Sie diese Installation oder ordnen Sie sie neu an, um standardmäßig den Desktop-Befehl zu verwenden. Sie können den Desktop-Befehl auch über seinen vollständigen Pfad ausführen.', + cliCommandSelectionUnknown: 'Ihr Shell-Befehl konnte nicht überprüft werden. Ein Alias oder eine andere dsh-Installation kann Vorrang haben.', + cliCommandInstalled: 'Der Desktop-Befehl ist installiert.', + cliCommandNotInstalled: 'Fügen Sie den Desktop-Befehl zu Ihrem Terminal hinzu.', + cliCommandBroken: 'Der Desktop-Befehl muss repariert werden.', + cliCommandInstall: 'Installieren', + cliCommandRepair: 'Reparieren', + cliCommandRemove: 'Entfernen', + cliCommandClose: 'Schließen', + cliCommandSwitch: 'Mit dem Desktop-Befehl fortfahren?', + cliCommandPreserve: 'Ein vorhandener Befehl bleibt erhalten. Andere Installationen und Shell-Startdateien werden nicht geändert.', + cliCommandContinue: 'Weiter', + cliCommandInstallApp: 'Installieren Sie Desktop in Ihrem Anwendungsordner, bevor Sie den Befehl dsh verwalten.', + cliCommandUpdating: 'Ein Update wird installiert. Verwalten Sie den Befehl nach Abschluss der Installation.', + cliCommandNewTerminal: 'Öffnen Sie ein neues Terminal und führen Sie dsh --version aus.', + cliCommandRemoved: 'Desktop-Befehlsregistrierung entfernt.', + cliCommandPreviousRestored: 'Der vorherige Launcher wurde wiederhergestellt.', + cliCommandOtherKept: 'Andere Befehlsinstallationen wurden beibehalten.', + cliCommandBackupKept: 'Ein früherer Launcher ist unter folgender Adresse erhalten: {path}', + cliCommandChanged: 'Der Befehl oder PATH hat sich geändert, während dieses Dialogfeld geöffnet war. Öffnen Sie den Befehl „dsh verwalten“ erneut, um den aktuellen Status zu überprüfen.', + cliCommandOwnershipError: 'Die Befehlsregistrierung oder Sicherung wurde geändert. Es wurde kein nicht verwandter Befehl entfernt.', + cliCommandFailed: 'Der Befehl konnte nicht aktualisiert werden. Überprüfen Sie, ob die Anwendung installiert ist und das Ziel beschreibbar ist, und versuchen Sie es dann erneut.', + application: 'Anwendung', + fileMenu: 'Datei', + closePage: 'Seite oder Fenster schließen', + aboutMenu: 'Über DeepSeek Harness', + aboutProduct: 'DeepSeek Harness', + aboutVersion: 'Version V{version}', + hideApplication: 'DeepSeek Harness ausblenden', + hideOtherApplications: 'Andere ausblenden', + showAllApplications: 'Alle einblenden', + quitApplication: 'DeepSeek Harness beenden', + openApplication: 'DeepSeek Harness öffnen', + quit: 'Beenden', + cancel: 'Abbrechen', + quitTitle: 'DeepSeek Harness beenden?', + quitActiveTasks: 'Laufende Aufgaben werden unterbrochen.', + quitScheduledTasks: 'Geplante Aufgaben werden nicht ausgeführt, während die App geschlossen ist.', + quitActiveAndScheduledTasks: 'Laufende Aufgaben werden unterbrochen und geplante Aufgaben werden nicht ausgeführt, während die App geschlossen ist.', + backgroundNoticeBody: 'Laufende Aufgaben werden fortgesetzt. Sie können das Fenster über die Taskleiste erneut öffnen.', + backgroundNoticeConfirm: 'Bestätigen', + edit: 'Bearbeiten', + menuBar: 'Anwendungsmenü', + delete: 'Löschen', + undo: 'Rückgängig machen', + redo: 'Wiederholen', + cut: 'Ausschneiden', + copy: 'Kopieren', + paste: 'Einfügen', + selectAll: 'Alles auswählen', + startupFailed: 'DeepSeek Harness ist nicht verfügbar', + fatalSummary: 'Die Anwendung konnte nicht gestartet werden oder wurde unerwartet gestoppt.', + startupAddressInUse: 'Eine andere DSH-Instanz (z. B. dsh Web oder die Desktop-App) wird ausgeführt. Sie können nicht gleichzeitig starten. Beenden Sie die andere laufende DSH-Instanz und starten Sie sie neu.', + diagnosticTruncated: '… Fehlerdetails gekürzt.', + reportWrittenTo: 'Diagnosebericht: {path}', + startupReinstallAdvice: 'Wenn Anwendungsdateien fehlen oder beschädigt sind, schließen Sie die Anwendung und installieren Sie sie erneut. Ihre Aufgaben werden separat gespeichert.', + exitApplication: 'Beenden', + restartApplication: 'Neustart', + recoveryOperationFailed: 'Der Wiederherstellungsvorgang ist fehlgeschlagen', + disableThirdPartyPlugins: 'Deaktivieren Sie Plugins von Drittanbietern, sichern Sie den Profil-Patch und starten Sie neu', + welcomeTitle: 'DeepSeek Harness', + welcomeBrand: 'DeepSeek Harness', + welcomeTaglineBefore: 'Willkommen bei ', + welcomeTaglineBrand: 'DeepSeek Harness', + welcomeTaglineAfter: '', + welcomeDescription: 'Potenzial aufbauen. Entdecken Sie Intelligenz.', + welcomeAuthStarting: 'Anmeldung wird geöffnet…', + welcomeAuthWaiting: 'Browser wurde nicht automatisch geöffnet?', + welcomeAuthWaitingDescription: 'Kopieren Sie den Anmeldelink und öffnen Sie ihn in Ihrem Browser, um sich anzumelden.', + welcomeAuthExchanging: 'Anmeldung wird abgeschlossen…', + welcomeAuthExpired: 'Zeitüberschreitung bei der Anmeldung', + welcomeAuthExpiredDescription: 'Melden Sie sich erneut an, um fortzufahren', + welcomeAuthFailed: 'Die Anmeldung konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut.', + welcomeAuthCopyLink: 'Anmeldelink kopieren', + welcomeAuthCopied: 'Kopiert', + welcomeAuthCopyFailed: 'Konnte nicht kopiert werden. Versuchen Sie es erneut.', + welcomeAuthCancel: 'Abbrechen', + welcomeAuthRetry: 'Erneut anmelden', + welcomeSignIn: 'Anmelden', + welcomeApiKey: 'API-Schlüssel hinzufügen', + welcomeKeyTitle: 'Fügen Sie einen API-Schlüssel hinzu, um loszulegen', + welcomeKeyDescription: 'Konfigurieren Sie offizielle DeepSeek-Modelle für die Verwendung von Harness', + welcomeKeyPlaceholder: 'API-Schlüssel eingeben', + welcomeKeySave: 'Speichern und fortfahren', + welcomeKeyLater: 'Später einrichten', + welcomeKeyBack: 'Zurück zur Anmeldung', + welcomeSessionExpired: 'Sie haben sich von Ihrem Konto abgemeldet. Bitte melden Sie sich erneut an.', + welcomeKeyBlank: 'Geben Sie einen API-Schlüssel ein.', + welcomeKeyInvalid: 'Geben Sie den API-Schlüssel selbst ein, ohne Anführungszeichen, Leerzeichen oder eine Umgebungsvariablenzuweisung.', + welcomeKeyFailed: 'Der API-Schlüssel konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.', + welcomeContinueFailed: 'Der Arbeitsbereich konnte nicht geöffnet werden. Bitte versuchen Sie es erneut.', + checkUpdatesMenu: 'Nach Updates suchen…', + reloadPageMenu: 'Seite neu laden', + restartAppHostMenu: 'Anwendung und Host neu starten', + updateCheckFailedTitle: 'Update-Überprüfung fehlgeschlagen', + updateCheckFailed: 'Es konnte nicht nach Updates gesucht werden. Bitte versuchen Sie es später noch einmal.', + updateDownloadFailed: 'Das Update konnte nicht heruntergeladen werden. Bitte versuchen Sie es erneut.', + updateInstallFailed: 'Das Update konnte nicht installiert werden. Bitte versuchen Sie es später noch einmal.', + updateCheckNetworkFailed: 'Es konnte nicht nach Updates gesucht werden. Überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.', + updateDownloadNetworkFailed: 'Das Update konnte nicht heruntergeladen werden. Überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.', + updateInstallNetworkFailed: 'Das Update konnte nicht installiert werden. Überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.', + unknownError: 'Unbekannter Fehler', + updateCheckTitle: 'Nach Updates suchen', + updateCurrentDetail: 'Aktuelle Version: {version}', + updateCurrent: 'Sie verwenden die neueste Version.', + updateChecking: 'Suche nach Updates…', + updateDownload: 'Update herunterladen', + updateDownloadedTitle: 'Die Version {version} ist zur Installation bereit', + updateDownloadedDetail: 'Die App wird während des Updates geschlossen und nach Abschluss automatisch wieder geöffnet.', + updateDownloadedDetailWindows: 'Die App wird während des Updates vorübergehend geschlossen und nach Abschluss automatisch wieder geöffnet.\n\nDas Update kann einige Zeit dauern. Bitte warten Sie und starten Sie die App während der Installation nicht erneut.', + updateClose: 'Schließen', + updateAcknowledge: 'OK', + updateLater: 'Später aktualisieren', + updateDownloading: '{percent}% wird heruntergeladen…', + updateVerifying: 'Aktualisierungsdateien werden überprüft…', + updateInstalling: 'Neustart wird vorbereitet…', + updateRetry: 'Update erneut versuchen', + updateActiveTasks: 'Aufgaben sind noch in Bearbeitung', + updateActiveTasksDetail: 'Durch die Aktualisierung werden die laufenden Aufgaben gestoppt und die App neu gestartet. Weitermachen?', + updateStopTasks: 'Aufgaben beenden und Update installieren', + updateTasksChanged: 'Neue Aufgaben haben begonnen. Bestätigen Sie erneut, um die Aufgaben zu stoppen und zu aktualisieren.', + updateTasksUnavailable: 'Der Aufgabenstatus ist nicht verfügbar. Versuchen Sie erneut, die Aktualisierung durchzuführen, wenn der Arbeitsbereich bereit ist.', + updateFullscreenTimeout: 'Der Vollbildmodus konnte nicht beendet werden. Beenden Sie den Vollbildmodus und versuchen Sie dann erneut, das Update durchzuführen.', + updateStopFailed: 'Die Aufgaben konnten nicht sicher gestoppt werden. Das Update wurde nicht installiert. Bitte versuchen Sie es später noch einmal.', + updateTechnicalDetails: 'Technische Details anzeigen', + updateTitle: 'DeepSeek Harness-Update', + updateAvailable: 'Neue Version verfügbar: {version}', + updateDetail: 'Sobald der Download abgeschlossen ist, können Sie das Update installieren und die App neu starten.', + installAndRestart: 'Installieren und neu starten', + later: 'Später', + updateFailedTitle: 'Aktualisierung fehlgeschlagen', + mandatoryTitle: 'Aktualisieren, um fortzufahren', + mandatoryDetail: 'Diese Version wird nicht mehr unterstützt. Aktualisieren, um fortzufahren. Laufende Aufgaben werden so lange ausgeführt, bis Sie die Installation bestätigen und neu starten.', + mandatoryUnavailable: 'Update-Anforderungen konnten nicht überprüft werden. Bitte versuchen Sie es später noch einmal.', + policyLoginTitle: 'Melden Sie sich bei der Testumgebung an', + policyLoginRequired: 'Melden Sie sich bei Feishu an, um die Update-Anforderungen für diesen Test-Build zu prüfen. Durch die Anmeldung wird kein Update heruntergeladen oder installiert.', + policyLogin: 'Melden Sie sich mit Feishu an', + policyLoginFailed: 'Die Feishu-Anmeldung wurde nicht abgeschlossen. Bitte versuchen Sie es erneut.', + policyLoginLoading: 'Anmeldeseite wird geladen…', + mandatoryNoRelease: 'Es wurde kein kompatibles Update gefunden. Überprüfen Sie es erneut oder wenden Sie sich an den Support.', + mandatoryRefresh: 'Erneut prüfen', + mandatoryPage: 'Von der offiziellen Website herunterladen', + mandatoryCopy: 'Download-Link kopieren', + mandatoryPageFailed: 'Die offizielle Download-Seite konnte nicht geöffnet werden. Kopieren Sie den Link und öffnen Sie ihn in Ihrem Browser.', + mandatoryViewDownload: 'Download-Seite öffnen', + mandatoryViewPageFailed: 'Die Download-Seite konnte nicht geöffnet werden. Bitte versuchen Sie es erneut.', + mandatoryActionFailed: 'Die Aktion ist fehlgeschlagen. Bitte versuchen Sie es erneut. Nach Abschluss des Updates können Sie die App wieder verwenden.', + mandatoryReady: 'Update bereit', + mandatoryVersion: 'Neue Version: {version}', + mandatoryReadyDetail: 'Die App wird während des Updates geschlossen und nach Abschluss automatisch wieder geöffnet.', + mandatoryDeferred: 'Laufende Aufgaben werden weiterhin ausgeführt. Schließen Sie das Update ab, bevor Sie die App erneut verwenden.', + mandatoryContinue: 'Update fortsetzen', + mandatoryInspecting: 'Aufgaben prüfen…', + mandatoryStopping: 'Aufgaben stoppen…', + mandatoryRestarting: 'Die App wird in Kürze neu gestartet. Bitte warten.', + mandatoryDownloadFailed: 'Die Update-Dateien konnten nicht heruntergeladen oder vorbereitet werden. Bitte versuchen Sie es erneut.', + mandatoryInstallFailed: 'Das Update wurde nicht installiert. Überprüfen Sie die Aufgaben erneut und versuchen Sie es erneut.', + mandatoryOpenHelp: 'Seite wurde nicht geöffnet?', + mandatoryReopen: 'Offizielle Downloadseite erneut öffnen', + mandatoryCopied: 'Link kopiert', + mandatoryCopyFailed: 'Der Link konnte nicht kopiert werden. Wählen Sie es aus und kopieren Sie es unten.', + mandatoryAddress: 'Download-Link', + mandatoryNotification: 'Kehren Sie zur Anwendung zurück, um die Installation zu bestätigen und neu zu starten.', +} satisfies DesktopMessages + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + cliCommandMenu: 'Управление командой dsh…', + cliCommandTitle: 'Управление командой dsh', + cliCommandLocation: 'Команда Desktop: {path}', + cliCommandSelected: 'Текущая команда dsh: {path}.', + cliCommandTarget: 'Текущая цель лаунчера: {path}', + cliCommandShadowed: 'Другой dsh имеет приоритет. Удалите или измените порядок установки, чтобы использовать команду «Desktop» по умолчанию. Вы также можете запустить команду «Desktop» по ее полному пути.', + cliCommandSelectionUnknown: 'Не удалось проверить вашу команду оболочки. Псевдоним или другая установка dsh может иметь приоритет.', + cliCommandInstalled: 'Команда «Desktop» установлена.', + cliCommandNotInstalled: 'Добавьте команду «Desktop» в свой терминал.', + cliCommandBroken: 'Команда Desktop требует ремонта.', + cliCommandInstall: 'Установить', + cliCommandRepair: 'Исправить', + cliCommandRemove: 'Удалить', + cliCommandClose: 'Закрыть', + cliCommandSwitch: 'Продолжить команду «Desktop»?', + cliCommandPreserve: 'Существующая команда будет сохранена. Другие установки и файлы запуска оболочки не будут изменены.', + cliCommandContinue: 'Продолжить', + cliCommandInstallApp: 'Установите Desktop в папку «Приложения», прежде чем управлять командой dsh.', + cliCommandUpdating: 'Идет установка обновления. Управляйте командой после завершения установки.', + cliCommandNewTerminal: 'Откройте новый терминал и запустите dsh --version.', + cliCommandRemoved: 'Регистрация команд Desktop удалена.', + cliCommandPreviousRestored: 'Предыдущий лаунчер восстановлен.', + cliCommandOtherKept: 'Остальные командные установки остались на месте.', + cliCommandBackupKept: 'Предыдущий лаунчер сохранился по адресу: {path}.', + cliCommandChanged: 'Команда или PATH изменилась, пока это диалоговое окно было открыто. Откройте команду «Управление dsh» еще раз, чтобы просмотреть текущее состояние.', + cliCommandOwnershipError: 'Регистрация команд или резервное копирование изменены. Ни одна несвязанная команда не была удалена.', + cliCommandFailed: 'Не удалось обновить команду. Убедитесь, что приложение установлено и место назначения доступно для записи, затем повторите попытку.', + application: 'Приложение', + fileMenu: 'Файл', + closePage: 'Закрыть страницу или окно', + aboutMenu: 'О DeepSeek Harness', + aboutProduct: 'DeepSeek Harness', + aboutVersion: 'Версия V{version}', + hideApplication: 'Скрыть DeepSeek Harness', + hideOtherApplications: 'Скрыть других', + showAllApplications: 'Показать все', + quitApplication: 'Выйти из DeepSeek Harness', + openApplication: 'Открыть DeepSeek Harness', + quit: 'Выйти', + cancel: 'Отмена', + quitTitle: 'Выйти из DeepSeek Harness?', + quitActiveTasks: 'Выполнение задач будет прервано.', + quitScheduledTasks: 'Запланированные задачи не будут выполняться, пока приложение закрыто.', + quitActiveAndScheduledTasks: 'Выполнение задач будет прервано, а запланированные задачи не будут выполняться, пока приложение закрыто.', + backgroundNoticeBody: 'Выполнение задач продолжится. Вы можете снова открыть окно из системного трея.', + backgroundNoticeConfirm: 'Подтвердить', + edit: 'Редактировать', + menuBar: 'Меню приложения', + delete: 'Удалить', + undo: 'Отменить', + redo: 'Повторить', + cut: 'Вырезать', + copy: 'Копировать', + paste: 'Вставить', + selectAll: 'Выбрать все', + startupFailed: 'DeepSeek Harness недоступен.', + fatalSummary: 'Приложение не могло запуститься или неожиданно остановилось.', + startupAddressInUse: 'Другой экземпляр DSH (например, веб-приложение dsh или настольное приложение) запущен. Они не могут начаться одновременно. Закройте другой работающий экземпляр DSH, а затем перезапустите.', + diagnosticTruncated: '… Подробности об ошибках сокращены.', + reportWrittenTo: 'Диагностический отчет: {path}', + startupReinstallAdvice: 'Если файлы приложения отсутствуют или повреждены, закройте приложение и переустановите его. Ваши задачи хранятся отдельно.', + exitApplication: 'Выход', + restartApplication: 'Перезапустить', + recoveryOperationFailed: 'Операция восстановления не удалась', + disableThirdPartyPlugins: 'Отключите сторонние плагины, создайте резервную копию патча профиля и перезапустите.', + welcomeTitle: 'DeepSeek Harness', + welcomeBrand: 'DeepSeek Harness', + welcomeTaglineBefore: 'Добро пожаловать в ', + welcomeTaglineBrand: 'DeepSeek Harness', + welcomeTaglineAfter: '', + welcomeDescription: 'Развивайте потенциал. Исследуйте интеллект.', + welcomeAuthStarting: 'Открытие входа в систему…', + welcomeAuthWaiting: 'Браузер не открылся автоматически?', + welcomeAuthWaitingDescription: 'Скопируйте ссылку для входа и откройте ее в браузере, чтобы войти.', + welcomeAuthExchanging: 'Завершение входа…', + welcomeAuthExpired: 'Время входа в систему истекло', + welcomeAuthExpiredDescription: 'Войдите еще раз, чтобы продолжить', + welcomeAuthFailed: 'Не удалось завершить вход. Повторите попытку.', + welcomeAuthCopyLink: 'Скопировать ссылку для входа', + welcomeAuthCopied: 'Скопировано', + welcomeAuthCopyFailed: 'Не удалось скопировать. Попробуйте еще раз.', + welcomeAuthCancel: 'Отмена', + welcomeAuthRetry: 'Войдите снова', + welcomeSignIn: 'Войти', + welcomeApiKey: 'Добавить ключ API', + welcomeKeyTitle: 'Добавьте ключ API, чтобы начать работу.', + welcomeKeyDescription: 'Настройте официальные модели DeepSeek, чтобы начать использовать Harness.', + welcomeKeyPlaceholder: 'Введите ключ API', + welcomeKeySave: 'Сохранить и продолжить', + welcomeKeyLater: 'Настроить позже', + welcomeKeyBack: 'Вернуться для входа в систему', + welcomeSessionExpired: 'Вы вышли из своей учетной записи, пожалуйста, войдите снова.', + welcomeKeyBlank: 'Введите ключ API.', + welcomeKeyInvalid: 'Введите сам ключ API без кавычек, пробелов и присвоения переменной среды.', + welcomeKeyFailed: 'Не удалось сохранить ключ API. Пожалуйста, попробуйте еще раз.', + welcomeContinueFailed: 'Не удалось открыть рабочую область. Пожалуйста, попробуйте еще раз.', + checkUpdatesMenu: 'Проверить наличие обновлений…', + reloadPageMenu: 'Обновить страницу', + restartAppHostMenu: 'Перезапустите приложение и хост.', + updateCheckFailedTitle: 'Проверка обновления не удалась', + updateCheckFailed: 'Не удалось проверить наличие обновлений. Пожалуйста, повторите попытку позже.', + updateDownloadFailed: 'Не удалось загрузить обновление. Пожалуйста, попробуйте еще раз.', + updateInstallFailed: 'Не удалось установить обновление. Пожалуйста, повторите попытку позже.', + updateCheckNetworkFailed: 'Не удалось проверить наличие обновлений. Проверьте подключение и повторите попытку.', + updateDownloadNetworkFailed: 'Не удалось загрузить обновление. Проверьте подключение и повторите попытку.', + updateInstallNetworkFailed: 'Не удалось установить обновление. Проверьте подключение и повторите попытку.', + unknownError: 'Неизвестная ошибка', + updateCheckTitle: 'Проверьте наличие обновлений', + updateCurrentDetail: 'Текущая версия: {version}.', + updateCurrent: 'У вас последняя версия.', + updateChecking: 'Проверка обновлений…', + updateDownload: 'Скачать обновление', + updateDownloadedTitle: 'Версия {version} готова к установке.', + updateDownloadedDetail: 'Приложение закроется во время обновления и автоматически откроется после его завершения.', + updateDownloadedDetailWindows: 'Приложение временно закроется во время обновления и автоматически откроется после его завершения.\n\nОбновление может занять некоторое время. Пожалуйста, подождите и не запускайте приложение снова во время установки.', + updateClose: 'Закрыть', + updateAcknowledge: 'OK', + updateLater: 'Обновить позже', + updateDownloading: 'Загрузка {percent}%…', + updateVerifying: 'Проверка файлов обновлений…', + updateInstalling: 'Подготовка к перезапуску…', + updateRetry: 'Повторить обновление', + updateActiveTasks: 'Задачи еще выполняются', + updateActiveTasksDetail: 'Обновление остановит выполняемые задачи и перезапустит приложение. Продолжать?', + updateStopTasks: 'Остановить задачи и обновить', + updateTasksChanged: 'Начались новые задачи. Подтвердите еще раз, чтобы остановить выполнение задач и выполнить обновление.', + updateTasksUnavailable: 'Статус задачи недоступен. Попробуйте обновить еще раз, когда рабочая область будет готова.', + updateFullscreenTimeout: 'Не удалось выйти из полноэкранного режима. Выйдите из полноэкранного режима и повторите попытку обновления.', + updateStopFailed: 'Не удалось безопасно остановить задачи. Обновление не установлено. Пожалуйста, повторите попытку позже.', + updateTechnicalDetails: 'Посмотреть технические подробности', + updateTitle: 'DeepSeek Harness Обновление', + updateAvailable: 'Доступна новая версия: {version}.', + updateDetail: 'После завершения загрузки вы можете установить обновление и перезапустить приложение.', + installAndRestart: 'Установить и перезапустить', + later: 'Позже', + updateFailedTitle: 'Обновление не выполнено', + mandatoryTitle: 'Обновите, чтобы продолжить', + mandatoryDetail: 'Эта версия больше не поддерживается. Обновите, чтобы продолжить. Выполняемые задачи будут продолжать выполняться до тех пор, пока вы не подтвердите установку и не перезапустите.', + mandatoryUnavailable: 'Не удалось проверить требования к обновлению. Пожалуйста, повторите попытку позже.', + policyLoginTitle: 'Войдите в тестовую среду', + policyLoginRequired: 'Войдите в систему с помощью Feishu, чтобы проверить требования к обновлению для этой тестовой сборки. При входе в систему обновление не загружается и не устанавливается.', + policyLogin: 'Войти через Feishu', + policyLoginFailed: 'Вход в Feishu не завершен. Пожалуйста, попробуйте еще раз.', + policyLoginLoading: 'Загрузка страницы входа…', + mandatoryNoRelease: 'Совместимое обновление не найдено. Проверьте еще раз или обратитесь в службу поддержки.', + mandatoryRefresh: 'Проверьте еще раз', + mandatoryPage: 'Скачать с официального сайта', + mandatoryCopy: 'Скопировать ссылку для скачивания', + mandatoryPageFailed: 'Не удалось открыть официальную страницу загрузки. Скопируйте ссылку и откройте ее в браузере.', + mandatoryViewDownload: 'Открыть страницу загрузки', + mandatoryViewPageFailed: 'Не удалось открыть страницу загрузки. Пожалуйста, попробуйте еще раз.', + mandatoryActionFailed: 'Не удалось выполнить действие. Повторите попытку. Вы сможете снова использовать приложение после завершения обновления.', + mandatoryReady: 'Обновление готово', + mandatoryVersion: 'Новая версия: {version}', + mandatoryReadyDetail: 'Приложение закроется во время обновления и автоматически откроется после его завершения.', + mandatoryDeferred: 'Выполняемые задачи будут продолжать выполняться. Завершите обновление, прежде чем снова использовать приложение.', + mandatoryContinue: 'Продолжить обновление', + mandatoryInspecting: 'Проверка задач…', + mandatoryStopping: 'Остановка задач…', + mandatoryRestarting: 'Приложение вскоре перезапустится. Пожалуйста, подождите.', + mandatoryDownloadFailed: 'Файлы обновления не удалось загрузить или подготовить. Пожалуйста, повторите попытку.', + mandatoryInstallFailed: 'Обновление не установлено. Проверьте задачи еще раз и повторите попытку.', + mandatoryOpenHelp: 'Страница не открылась?', + mandatoryReopen: 'Откройте официальную страницу загрузки еще раз.', + mandatoryCopied: 'Ссылка скопирована', + mandatoryCopyFailed: 'Не удалось скопировать ссылку. Выделите и скопируйте его ниже.', + mandatoryAddress: 'Ссылка для скачивания', + mandatoryNotification: 'Вернитесь в приложение, чтобы подтвердить установку и перезапустить.', +} satisfies DesktopMessages + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + cliCommandMenu: 'Gerenciar comando dsh…', + cliCommandTitle: 'Gerenciar comando dsh', + cliCommandLocation: 'Comando da área de trabalho: {path}', + cliCommandSelected: 'Comando dsh atual: {path}', + cliCommandTarget: 'Alvo atual do iniciador: {path}', + cliCommandShadowed: 'Outro dsh tem precedência. Remova ou reordene essa instalação para usar o comando Desktop por padrão. Você também pode executar o comando Desktop pelo caminho completo.', + cliCommandSelectionUnknown: 'Seu comando shell não pôde ser verificado. Um alias ou outra instalação do dsh pode ter precedência.', + cliCommandInstalled: 'O comando Desktop está instalado.', + cliCommandNotInstalled: 'Adicione o comando Desktop ao seu terminal.', + cliCommandBroken: 'O comando Desktop precisa de reparo.', + cliCommandInstall: 'Instalar', + cliCommandRepair: 'Reparar', + cliCommandRemove: 'Remover', + cliCommandClose: 'Fechar', + cliCommandSwitch: 'Continuar com o comando Desktop?', + cliCommandPreserve: 'Um comando existente será preservado. Outras instalações e arquivos de inicialização do shell não serão alterados.', + cliCommandContinue: 'Continuar', + cliCommandInstallApp: 'Instale o Desktop na pasta Aplicativos antes de gerenciar o comando dsh.', + cliCommandUpdating: 'Uma atualização está sendo instalada. Gerencie o comando após a conclusão da instalação.', + cliCommandNewTerminal: 'Abra um novo terminal e execute dsh --version.', + cliCommandRemoved: 'Registro de comando da área de trabalho removido.', + cliCommandPreviousRestored: 'O inicializador anterior foi restaurado.', + cliCommandOtherKept: 'Outras instalações de comando foram mantidas.', + cliCommandBackupKept: 'Um inicializador anterior é preservado em: {path}', + cliCommandChanged: 'O comando ou PATH foi alterado enquanto esta caixa de diálogo estava aberta. Abra o comando Gerenciar dsh novamente para revisar o estado atual.', + cliCommandOwnershipError: 'O registro ou backup do comando foi alterado. Nenhum comando não relacionado foi removido.', + cliCommandFailed: 'O comando não pôde ser atualizado. Verifique se o aplicativo está instalado e se o destino é gravável e tente novamente.', + application: 'Aplicativo', + fileMenu: 'Arquivo', + closePage: 'Fechar página ou janela', + aboutMenu: 'Sobre DeepSeek Harness', + aboutProduct: 'DeepSeek Harness', + aboutVersion: 'Versão V{version}', + hideApplication: 'Ocultar DeepSeek Harness', + hideOtherApplications: 'Ocultar outros', + showAllApplications: 'Mostrar tudo', + quitApplication: 'Sair de DeepSeek Harness', + openApplication: 'Abra DeepSeek Harness', + quit: 'Sair', + cancel: 'Cancelar', + quitTitle: 'Sair de DeepSeek Harness?', + quitActiveTasks: 'As tarefas em execução serão interrompidas.', + quitScheduledTasks: 'As tarefas agendadas não serão executadas enquanto o aplicativo estiver fechado.', + quitActiveAndScheduledTasks: 'As tarefas em execução serão interrompidas e as tarefas agendadas não serão executadas enquanto o aplicativo estiver fechado.', + backgroundNoticeBody: 'As tarefas em execução continuarão. Você pode reabrir a janela na bandeja do sistema.', + backgroundNoticeConfirm: 'Confirmar', + edit: 'Editar', + menuBar: 'Menu de aplicativos', + delete: 'Excluir', + undo: 'Desfazer', + redo: 'Refazer', + cut: 'Cortar', + copy: 'Copiar', + paste: 'Colar', + selectAll: 'Selecionar tudo', + startupFailed: 'DeepSeek Harness não está disponível', + fatalSummary: 'O aplicativo não pôde ser iniciado ou parou inesperadamente.', + startupAddressInUse: 'Outra instância DSH (como dsh web ou aplicativo de desktop) está em execução. Eles não podem começar ao mesmo tempo. Saia da outra instância DSH em execução e reinicie.', + diagnosticTruncated: '… Detalhes do erro reduzidos.', + reportWrittenTo: 'Relatório de diagnóstico: {path}', + startupReinstallAdvice: 'Se os arquivos do aplicativo estiverem ausentes ou danificados, feche o aplicativo e reinstale-o. Suas tarefas são armazenadas separadamente.', + exitApplication: 'Sair', + restartApplication: 'Reiniciar', + recoveryOperationFailed: 'A operação de recuperação falhou', + disableThirdPartyPlugins: 'Desative plug-ins de terceiros, faça backup do patch do perfil e reinicie', + welcomeTitle: 'DeepSeek Harness', + welcomeBrand: 'DeepSeek Harness', + welcomeTaglineBefore: 'Bem-vindo ao ', + welcomeTaglineBrand: 'DeepSeek Harness', + welcomeTaglineAfter: '', + welcomeDescription: 'Construir potencial. Explore a inteligência.', + welcomeAuthStarting: 'Abrindo login…', + welcomeAuthWaiting: 'O navegador não abriu automaticamente?', + welcomeAuthWaitingDescription: 'Copie o link de login e abra-o em seu navegador para fazer login.', + welcomeAuthExchanging: 'Concluindo login…', + welcomeAuthExpired: 'O tempo limite de login expirou', + welcomeAuthExpiredDescription: 'Faça login novamente para continuar', + welcomeAuthFailed: 'Não foi possível concluir o login. Tente novamente.', + welcomeAuthCopyLink: 'Copiar link de login', + welcomeAuthCopied: 'Copiado', + welcomeAuthCopyFailed: 'Não foi possível copiar. Tente novamente.', + welcomeAuthCancel: 'Cancelar', + welcomeAuthRetry: 'Faça login novamente', + welcomeSignIn: 'Faça login', + welcomeApiKey: 'Adicionar chave API', + welcomeKeyTitle: 'Adicione uma chave API para começar', + welcomeKeyDescription: 'Configure modelos DeepSeek oficiais para começar a usar o Harness', + welcomeKeyPlaceholder: 'Insira a chave de API', + welcomeKeySave: 'Salvar e continuar', + welcomeKeyLater: 'Configurar mais tarde', + welcomeKeyBack: 'Voltar para fazer login', + welcomeSessionExpired: 'Você saiu da sua conta, faça login novamente.', + welcomeKeyBlank: 'Insira uma chave API.', + welcomeKeyInvalid: 'Insira a própria chave API, sem aspas, espaços ou atribuição de variável de ambiente.', + welcomeKeyFailed: 'Não foi possível salvar a chave API. Por favor, tente novamente.', + welcomeContinueFailed: 'Não foi possível abrir o espaço de trabalho. Por favor, tente novamente.', + checkUpdatesMenu: 'Verifique se há atualizações…', + reloadPageMenu: 'Recarregar página', + restartAppHostMenu: 'Reinicie o aplicativo e o host', + updateCheckFailedTitle: 'Falha na verificação de atualização', + updateCheckFailed: 'Não foi possível verificar se há atualizações. Por favor, tente novamente mais tarde.', + updateDownloadFailed: 'Não foi possível baixar a atualização. Por favor, tente novamente.', + updateInstallFailed: 'Não foi possível instalar a atualização. Por favor, tente novamente mais tarde.', + updateCheckNetworkFailed: 'Não foi possível verificar se há atualizações. Verifique sua conexão e tente novamente.', + updateDownloadNetworkFailed: 'Não foi possível baixar a atualização. Verifique sua conexão e tente novamente.', + updateInstallNetworkFailed: 'Não foi possível instalar a atualização. Verifique sua conexão e tente novamente.', + unknownError: 'Erro desconhecido', + updateCheckTitle: 'Verifique se há atualizações', + updateCurrentDetail: 'Versão atual: {version}', + updateCurrent: 'Você está atualizado!', + updateChecking: 'Verificando atualizações…', + updateDownload: 'Baixar atualização', + updateDownloadedTitle: 'A versão {version} está pronta para instalação', + updateDownloadedDetail: 'O aplicativo será fechado durante a atualização e reaberto automaticamente quando for concluída.', + updateDownloadedDetailWindows: 'O aplicativo será fechado temporariamente durante a atualização e reaberto automaticamente quando for concluída.\n\nA atualização pode levar algum tempo. Aguarde e não inicie o aplicativo novamente durante a instalação.', + updateClose: 'Fechar', + updateAcknowledge: 'OK', + updateLater: 'Atualizar mais tarde', + updateDownloading: 'Baixando {percent}%…', + updateVerifying: 'Verificando arquivos de atualização…', + updateInstalling: 'Preparando para reiniciar…', + updateRetry: 'Tentar atualizar novamente', + updateActiveTasks: 'As tarefas ainda estão em andamento', + updateActiveTasksDetail: 'A atualização interromperá as tarefas em andamento e reiniciará o aplicativo. Continuar?', + updateStopTasks: 'Pare tarefas e atualize', + updateTasksChanged: 'Novas tarefas foram iniciadas. Confirme novamente para interromper as tarefas e atualizar.', + updateTasksUnavailable: 'O status da tarefa não está disponível. Tente atualizar novamente quando o espaço de trabalho estiver pronto.', + updateFullscreenTimeout: 'Não foi possível sair da tela cheia. Saia da tela inteira e tente atualizar novamente.', + updateStopFailed: 'Não foi possível interromper as tarefas com segurança. A atualização não foi instalada. Por favor, tente novamente mais tarde.', + updateTechnicalDetails: 'Ver detalhes técnicos', + updateTitle: 'DeepSeek Harness Atualização', + updateAvailable: 'Nova versão disponível: {version}', + updateDetail: 'Assim que o download for concluído, você pode instalar a atualização e reiniciar o aplicativo.', + installAndRestart: 'Instalar e reiniciar', + later: 'Mais tarde', + updateFailedTitle: 'Falha na atualização', + mandatoryTitle: 'Atualizar para continuar', + mandatoryDetail: 'Esta versão não é mais suportada. Atualize para continuar. As tarefas em andamento continuarão em execução até você confirmar a instalação e reiniciar.', + mandatoryUnavailable: 'Não foi possível verificar os requisitos de atualização. Por favor, tente novamente mais tarde.', + policyLoginTitle: 'Faça login no ambiente de teste', + policyLoginRequired: 'Faça login com Feishu para verificar os requisitos de atualização para esta versão de teste. O login não baixará nem instalará uma atualização.', + policyLogin: 'Faça login com Feishu', + policyLoginFailed: 'O login do Feishu não foi concluído. Por favor, tente novamente.', + policyLoginLoading: 'Carregando página de login…', + mandatoryNoRelease: 'Nenhuma atualização compatível foi encontrada. Verifique novamente ou entre em contato com o suporte.', + mandatoryRefresh: 'Verifique novamente', + mandatoryPage: 'Baixe do site oficial', + mandatoryCopy: 'Copiar link de download', + mandatoryPageFailed: 'Não foi possível abrir a página oficial de download. Copie o link e abra-o no seu navegador.', + mandatoryViewDownload: 'Abrir página de download', + mandatoryViewPageFailed: 'Não foi possível abrir a página de download. Por favor, tente novamente.', + mandatoryActionFailed: 'A ação falhou. Por favor, tente novamente. Você poderá usar o aplicativo novamente após a conclusão da atualização.', + mandatoryReady: 'Atualização pronta', + mandatoryVersion: 'Nova versão: {version}', + mandatoryReadyDetail: 'O aplicativo será fechado durante a atualização e reaberto automaticamente quando for concluída.', + mandatoryDeferred: 'As tarefas em andamento continuarão em execução. Conclua a atualização antes de usar o aplicativo novamente.', + mandatoryContinue: 'Continuar atualização', + mandatoryInspecting: 'Verificando tarefas…', + mandatoryStopping: 'Parando tarefas…', + mandatoryRestarting: 'O aplicativo será reiniciado em breve. Por favor, aguarde.', + mandatoryDownloadFailed: 'Os arquivos de atualização não puderam ser baixados ou preparados. Por favor, tente novamente.', + mandatoryInstallFailed: 'A atualização não foi instalada. Verifique as tarefas novamente e tente novamente.', + mandatoryOpenHelp: 'A página não abriu?', + mandatoryReopen: 'Abra a página oficial de download novamente', + mandatoryCopied: 'Link copiado', + mandatoryCopyFailed: 'Não foi possível copiar o link. Selecione e copie abaixo.', + mandatoryAddress: 'Link de download', + mandatoryNotification: 'Retorne ao aplicativo para confirmar a instalação e reinicie.', +} satisfies DesktopMessages + +const dictionaries = { en, zh, ja, es, fr, de, ru, pt } + /** Locale payload exposed to the Desktop-owned renderer. */ export interface DesktopLocale { - readonly id: 'en' | 'zh-CN' | 'ja' + readonly id: 'en' | 'zh-CN' | 'ja' | 'es' | 'fr' | 'de' | 'ru' | 'pt' readonly messages: DesktopMessages } /** Resolve Electron's locale to one shipped Desktop dictionary. */ export function resolveDesktopLocale(locale: string): DesktopLocale { const primary = locale.toLowerCase().split('-')[0] - if (primary === 'ja') return { id: 'ja', messages: ja } - return primary === 'zh' - ? { id: 'zh-CN', messages: zh } - : { id: 'en', messages: en } + if (primary !== undefined && Object.hasOwn(dictionaries, primary)) { + const id = primary as keyof typeof dictionaries + return { id: id === 'zh' ? 'zh-CN' : id, messages: dictionaries[id] } + } + return { id: 'en', messages: en } } /** @@ -555,10 +1418,10 @@ export function resolveDesktopLocale(locale: string): DesktopLocale { */ export function resolveDesktopStartupLocale(preference: string | null, languages: readonly string[]): DesktopLocale { const selected = preference?.toLowerCase() - if (selected === 'zh' || selected === 'en' || selected === 'ja') return resolveDesktopLocale(selected) + if (selected !== undefined && Object.hasOwn(dictionaries, selected)) return resolveDesktopLocale(selected) for (const language of languages) { const primary = language.toLowerCase().split('-')[0] - if (primary === 'zh' || primary === 'en' || primary === 'ja') return resolveDesktopLocale(primary) + if (primary !== undefined && Object.hasOwn(dictionaries, primary)) return resolveDesktopLocale(primary) } return resolveDesktopLocale('en') } diff --git a/apps/desktop/tests/installer-locales.spec.ts b/apps/desktop/tests/installer-locales.spec.ts index 4dea51a414..ba842d8e18 100644 --- a/apps/desktop/tests/installer-locales.spec.ts +++ b/apps/desktop/tests/installer-locales.spec.ts @@ -1,3 +1,5 @@ +import { SHIPPED_LOCALE_IDS } from '../../../scripts/shipped-locales.ts' +import { createRequire } from 'node:module' /** Per-language installer resources feed both the packaging language list and the NSIS compiler. */ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' @@ -8,7 +10,7 @@ import { readInstallerLocales, renderInstallerStrings } from '../scripts/install function fixture(): string { const directory = mkdtempSync(join(tmpdir(), 'dsh-installer-locales-')) onTestFinished(() => { rmSync(directory, { recursive: true, force: true }) }) - for (const id of ['en_US', 'zh_CN', 'ja_JP']) { + for (const id of ['en_US', 'zh_CN', 'ja_JP', 'es_ES', 'fr_FR', 'de_DE', 'ru_RU', 'pt_BR']) { writeFileSync(join(directory, `${id}.json`), readFileSync(new URL(`../installer/locales/${id}.json`, import.meta.url))) } return directory @@ -16,17 +18,17 @@ function fixture(): string { it('generates Japanese NSIS strings from the same files that select packaged languages', () => { const locales = readInstallerLocales() - expect(locales.map(locale => locale.id)).toEqual(['en_US', 'ja_JP', 'zh_CN']) + expect(locales.map(locale => locale.id)).toEqual(['en_US', 'de_DE', 'es_ES', 'fr_FR', 'ja_JP', 'pt_BR', 'ru_RU', 'zh_CN']) const source = renderInstallerStrings(locales) expect(source).toContain('LangString INSTALLER_INSTALL ${LANG_JAPANESE} "今すぐインストール"') expect(source).toContain('LangString INSTALLER_NATIVE_PROGRESS ${LANG_JAPANESE} "インストールを準備しています… %d%%"') expect(source).toContain('!ifdef LANG_JAPANESE\n') - expect(source.match(/^LangString /gmu)).toHaveLength(93) + expect(source.match(/^LangString /gmu)).toHaveLength(248) const japanese = locales.filter(locale => locale.id === 'ja_JP') expect(renderInstallerStrings(japanese)).not.toContain('${LANG_ENGLISH}') }) -it.each(['en_US', 'zh_CN', 'ja_JP'])('rejects a missing %s configuration', (id) => { +it.each(['en_US', 'zh_CN', 'ja_JP', 'es_ES', 'fr_FR', 'de_DE', 'ru_RU', 'pt_BR'])('rejects a missing %s configuration', (id) => { const directory = fixture() rmSync(join(directory, `${id}.json`)) expect(() => readInstallerLocales(directory)).toThrow(`missing ${id}.json`) @@ -39,7 +41,10 @@ it.each([ ['changed placeholder', (resource: ReturnType[number]) => { resource.strings.INSTALLER_NATIVE_PROGRESS = '%s%%' }, 'format placeholders'], ['missing percent escape', (resource: ReturnType[number]) => { resource.strings.INSTALLER_NATIVE_PROGRESS = '%d%' }, 'format placeholders'], ['extra format specifier', (resource: ReturnType[number]) => { resource.strings.INSTALLER_NATIVE_PROGRESS = '%d%% %q' }, 'format placeholders'], + ['translated command', (resource: ReturnType[number]) => { resource.strings.INSTALLER_THEME_ERROR = 'Use /THEME=claro.' }, 'format placeholders'], ['duplicate language', (resource: ReturnType[number]) => { resource.nsisLanguage = 'ENGLISH' }, 'duplicate NSIS language'], + ['invisible character', (resource: ReturnType[number]) => { resource.strings.INSTALLER_INSTALL = 'Install\u200b' }, 'invisible'], + ['byte-order mark', (resource: ReturnType[number]) => { resource.strings.INSTALLER_INSTALL = 'Install\ufeff' }, 'invisible'], ['invalid compiler token', (resource: ReturnType[number]) => { resource.nsisLanguage = 'JA\n!error' }, 'invalid NSIS language'], ])('rejects %s', (_name, mutate, diagnostic) => { const directory = fixture() @@ -54,3 +59,18 @@ it('escapes literal NSIS syntax without changing translated text', () => { expect(renderInstallerStrings([{ ...locale, strings: { INSTALLER_INSTALL: '"$name"\nnext\rline' } }])) .toContain('"$\\"$$name$\\"$\\nnext$\\rline"') }) + +it('uses the real electron-builder language identifiers and English fallback', () => { + const require = createRequire(import.meta.url) + const { LangConfigurator, createAddLangsMacro } = require('app-builder-lib/out/targets/nsis/nsisLang.js') as { + LangConfigurator: new (options: { installerLanguages: string[] }) => object + createAddLangsMacro: (generator: { macro: (name: string, lines: string[]) => void }, languages: object) => void + } + const locales = readInstallerLocales() + expect(locales[0]?.id).toBe('en_US') + expect(locales.map(locale => locale.id.split('_')[0]).sort()).toEqual([...SHIPPED_LOCALE_IDS].sort()) + createAddLangsMacro({ macro: (_name, lines) => { + expect(lines.map(line => /MUI_LANGUAGE "([^" ]+)"/u.exec(line)?.[1]?.toUpperCase())) + .toEqual(locales.map(locale => locale.nsisLanguage)) + } }, new LangConfigurator({ installerLanguages: locales.map(locale => locale.id) })) +}) diff --git a/apps/desktop/tests/installer-packaging.spec.ts b/apps/desktop/tests/installer-packaging.spec.ts index 57da6058df..1e438dedf4 100644 --- a/apps/desktop/tests/installer-packaging.spec.ts +++ b/apps/desktop/tests/installer-packaging.spec.ts @@ -1,3 +1,4 @@ +import { readInstallerLocales } from '../scripts/installer-locales.mjs' import { tmpdir } from 'node:os' import { readFileSync } from 'node:fs' import { Arch, Platform } from 'electron-builder' @@ -54,7 +55,7 @@ describe('installer preparation preserves application dependencies', () => { expect(appInfo.copyright).toBe(`Copyright © ${new Date().getFullYear()} DeepSeek Harness`) if (platform === 'win32') { expect(expandMacro(config.nsis.uninstallDisplayName, null, appInfo, {}, false)).toBe('DeepSeek Harness') - expect(config.nsis.installerLanguages).toEqual(['en_US', 'ja_JP', 'zh_CN']) + expect(config.nsis.installerLanguages).toEqual(readInstallerLocales().map(locale => locale.id)) } const aboutIcon = config.extraResources.find(resource => resource.to === 'icon.png') expect(aboutIcon).toBeDefined() diff --git a/apps/desktop/tests/locale.spec.ts b/apps/desktop/tests/locale.spec.ts index eb9f2ccb81..b53a02e8fb 100644 --- a/apps/desktop/tests/locale.spec.ts +++ b/apps/desktop/tests/locale.spec.ts @@ -1,14 +1,34 @@ +import { SHIPPED_LOCALE_IDS } from '../../../scripts/shipped-locales.ts' import { describe, expect, it } from 'vitest' -import { en, formatDesktopMessage, resolveDesktopLocale, resolveDesktopStartupLocale, ja, zh } from '../src/locale.ts' +import { en, formatDesktopMessage, resolveDesktopLocale, resolveDesktopStartupLocale, ja, zh, es, fr, de, ru, pt } from '../src/locale.ts' describe('desktop locale dictionaries', () => { - it('ships the same key set in English, Chinese, and Japanese', () => { + it('ships baseline and Japanese keys and falls back for unsupported languages', () => { expect(Object.keys(zh)).toEqual(Object.keys(en)) expect(Object.keys(ja)).toEqual(Object.keys(en)) expect(resolveDesktopLocale('JA-jp')).toEqual({ id: 'ja', messages: ja }) expect(resolveDesktopLocale('zh-Hans-CN').messages).toEqual(zh) expect(resolveDesktopLocale('en-US').messages).toEqual(en) - expect(resolveDesktopLocale('fr-FR').messages).toEqual(en) + expect(resolveDesktopLocale('it-IT').messages).toEqual(en) + }) + + it.each([ + ['es', 'es-MX', es], ['fr', 'fr-CA', fr], ['de', 'de-DE', de], + ['ru', 'ru-RU', ru], ['pt', 'pt-BR', pt], + ] as const)('resolves %s, preserves placeholders, and honors saved preferences', (id, tag, messages) => { + expect(resolveDesktopLocale(tag.toUpperCase())).toEqual({ id, messages }) + expect(resolveDesktopStartupLocale(id, ['zh-CN']).id).toBe(id) + expect(resolveDesktopStartupLocale(null, ['it-IT', tag, 'en-US']).id).toBe(id) + expect(Object.keys(messages)).toEqual(Object.keys(en)) + for (const key of Object.keys(en) as (keyof typeof en)[]) { + const placeholders = (text: string): string[] => (text.match(/\{\w+\}/gu) ?? []).sort() + expect(placeholders(messages[key]), key).toEqual(placeholders(en[key])) + } + }) + + it.each(SHIPPED_LOCALE_IDS)('resolves every shipped language %s without fallback', (id) => { + expect(resolveDesktopLocale(id).id).toBe(id === 'zh' ? 'zh-CN' : id) + expect(resolveDesktopStartupLocale(null, [id]).id).toBe(id === 'zh' ? 'zh-CN' : id) }) it('formats named values without consuming unknown placeholders', () => { diff --git a/apps/desktop/tests/macos-signature.spec.ts b/apps/desktop/tests/macos-signature.spec.ts index afe2092791..2e6a2bd48e 100644 --- a/apps/desktop/tests/macos-signature.spec.ts +++ b/apps/desktop/tests/macos-signature.spec.ts @@ -1,3 +1,4 @@ +import { SHIPPED_LOCALE_IDS } from '../../../scripts/shipped-locales.ts' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import { readFileSync } from 'node:fs' import { createRequire } from 'node:module' @@ -51,7 +52,7 @@ describe('desktop macOS release signature', () => { expect(config.protocols).toEqual([{ name: 'DeepSeek Harness', schemes: ['dsh'] }]) expect(portablePath(config.directories.output)).toContain('/.desktop-build/targets/mac-arm64/artifacts') expect(config.mac.extendInfo.NSMicrophoneUsageDescription).toContain('microphone') - expect(config.mac.extendInfo.CFBundleLocalizations).toEqual(['en', 'zh_CN', 'ja']) + expect(config.mac.extendInfo.CFBundleLocalizations).toEqual(SHIPPED_LOCALE_IDS.map(id => id === 'zh' ? 'zh_CN' : id)) expect(config.mac.entitlementsInherit).toBe(config.mac.entitlements) const entitlements = readFileSync(config.mac.entitlements, 'utf8') for (const key of ['com.apple.security.cs.allow-jit', 'com.apple.security.cs.allow-unsigned-executable-memory', diff --git a/apps/web/tests/additional-locales.e2e.ts b/apps/web/tests/additional-locales.e2e.ts new file mode 100644 index 0000000000..007aab3e14 --- /dev/null +++ b/apps/web/tests/additional-locales.e2e.ts @@ -0,0 +1,61 @@ +/** Shipped language selection, metadata and narrow settings through the assembled Web profile. */ +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { chromium, type Browser } from 'playwright' +import { expect, it, onTestFailed, onTestFinished } from 'vitest' +import { acknowledgeReloadConnectionLoss, captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts' +import { saveFailureShot } from './support.ts' + +const languages = [ + ['es', 'es-MX', 'Menú de cuenta', 'Configuración', 'Complementos', 'Idioma', 'Español'], + ['fr', 'fr-CA', 'Menu du compte', 'Paramètres', 'Plugins', 'Langue', 'Français'], + ['de', 'de-DE', 'Kontomenü', 'Einstellungen', 'Plugins', 'Sprache', 'Deutsch'], + ['ru', 'ru-RU', 'Меню аккаунта', 'Настройки', 'Плагины', 'Язык', 'Русский'], + ['pt', 'pt-BR', 'Menu da conta', 'Configurações', 'Plug-ins', 'Idioma', 'Português'], +] as const + +it.each(languages)('selects %s from the browser, localizes metadata and persists an explicit choice', async (id, tag, accountMenu, settings, plugins, language, label) => { + const resources: { scaffold?: WebScaffold; browser?: Browser } = {} + onTestFinished(async () => { + try { await resources.browser?.close() } finally { await resources.scaffold?.close() } + }) + const scaffold = await launchWebScaffold() + resources.scaffold = scaffold + const browser = await chromium.launch() + resources.browser = browser + const page = await browser.newPage({ locale: tag, viewport: { width: 1440, height: 960 } }) + const tripwire = watchConsole(page) + onTestFailed(() => saveFailureShot(page, `additional-locale-${id}`)) + await page.goto(scaffold.authenticatedUrl) + await expect.poll(() => page.evaluate(() => document.documentElement.lang)).toBe(id) + await page.getByRole('button', { name: plugins, exact: true }).click() + const panel = page.locator('[data-plugin-panel]') + const metadata = JSON.parse(await readFile(new URL(`../../../packages/experimental/auto-review/locale/${id}.json`, import.meta.url), 'utf8')) as { meta: { title: string; description: string } } + await panel.getByText(metadata.meta.title, { exact: true }).waitFor() + await panel.getByText(metadata.meta.description, { exact: true }).waitFor() + await compareOrRefreshGolden(fileURLToPath(new URL(`./expected/additional-locales/${id}.expected.md`, import.meta.url)), + await captureStableAria(page, `[data-plugin-panel] li:has-text(${JSON.stringify(metadata.meta.title)})`, scaffold.workspaceCwd), webSnapshotMode()) + await page.getByRole('button', { name: accountMenu, exact: true }).click() + await page.getByRole('menuitem', { name: settings, exact: true }).click() + const dialog = page.getByRole('dialog', { name: settings, exact: true }) + await dialog.getByText(language, { exact: true }).waitFor() + await dialog.getByRole('button', { name: label, exact: true }).waitFor() + await page.setViewportSize({ width: 390, height: 844 }) + await expect.poll(() => dialog.evaluate((element) => { + const content = element.lastElementChild! + return { panel: element.scrollWidth <= element.clientWidth + 1, content: content.scrollWidth <= content.clientWidth + 1 } + })).toEqual({ panel: true, content: true }) + await dialog.getByRole('button', { name: label, exact: true }).click() + await page.getByRole('menuitem', { name: label, exact: true }).click() + await expect.poll(() => readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).toContain(`preference: ${id}`) + await dialog.getByRole('button', { name: label, exact: true }).click() + await page.getByRole('menuitem', { name: 'English', exact: true }).click() + await expect.poll(() => readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).toContain('preference: en') + const warningsBeforeReload = tripwire.warnings.length + await page.reload() + acknowledgeReloadConnectionLoss(tripwire, warningsBeforeReload) + await expect.poll(() => page.evaluate(() => document.documentElement.lang)).toBe('en') + expect(tripwire.warnings).toEqual([]) + expect(tripwire.pageErrors).toEqual([]) +}) diff --git a/apps/web/tests/conversation-view-tabs.e2e.ts b/apps/web/tests/conversation-view-tabs.e2e.ts new file mode 100644 index 0000000000..3995e03b45 --- /dev/null +++ b/apps/web/tests/conversation-view-tabs.e2e.ts @@ -0,0 +1,110 @@ +// Browser hit testing for the conversation view tabs: each tab accepts presses +// beyond its label, and the selected tab's bar stays above the Trajectory +// view's sticky toolbar where the two meet. +import type { Browser, Page } from 'playwright' +import { chromium } from 'playwright' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { createChatScrollFixture } from './chat-scroll-fixture.ts' +import { launchWebScaffold, seedSession, watchConsole, type WebScaffold } from './scaffold.ts' +import { newEnglishPage, saveFailureShot } from './support.ts' + +const FIXTURE = createChatScrollFixture({ + markerPrefix: 'VIEW_TABS', + title: 'VIEW_TABS short session', + turns: 4, +}) +const SEED_ID = 'conversation-view-tabs-web-e2e' + +/** Label and button rectangles of one tab, and which element owns each probe point. */ +interface TabProbe { + readonly label: { left: number; top: number; right: number; bottom: number } + readonly button: { left: number; top: number; right: number; bottom: number } + readonly ownsAboveLabel: boolean + readonly ownsBesideLabel: boolean + readonly ownsBarOverhang: boolean +} + +/** + * Probe one tab: the run above and beside its label, and the bar's lowest + * half pixel, below the button box. + * @param page - the page under test. + * @param name - the tab's accessible name. + * @returns the tab's rectangles and probe owners. + */ +async function probeTab(page: Page, name: string): Promise { + return page.getByRole('tab', { name, exact: true }).evaluate((tab) => { + const range = document.createRange() + range.selectNodeContents(tab) + const label = range.getBoundingClientRect() + const button = tab.getBoundingClientRect() + const owns = (x: number, y: number): boolean => document.elementFromPoint(x, y)?.closest('[role="tab"]') === tab + const centerX = (label.left + label.right) / 2 + return { + label: { left: label.left, top: label.top, right: label.right, bottom: label.bottom }, + button: { left: button.left, top: button.top, right: button.right, bottom: button.bottom }, + ownsAboveLabel: owns(centerX, label.top - 8), + ownsBesideLabel: owns(label.left - 8, (label.top + label.bottom) / 2), + ownsBarOverhang: owns(centerX, button.bottom + 0.5), + } + }) +} + +async function openSeededSession(page: Page): Promise { + const searchButton = page.getByRole('button', { name: 'Search sessions' }) + if (await searchButton.getAttribute('aria-expanded') !== 'true') await searchButton.click() + await page.getByRole('textbox', { name: 'Search session names', exact: true }).fill(FIXTURE.markers.user(1)) + const results = page.getByRole('tree', { name: 'Search results' }).getByRole('treeitem') + const deadline = Date.now() + 60_000 + while (await results.count() !== 1) { + if (Date.now() > deadline) throw new Error('seeded session never appeared in the sidebar search results') + await page.waitForTimeout(200) + } + await results.click() +} + +describe('web e2e: conversation view tabs', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + + beforeAll(async () => { + scaffold = await launchWebScaffold({}) + await seedSession(scaffold, FIXTURE.log, SEED_ID) + browser = await chromium.launch() + page = await newEnglishPage(browser, 900) + tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + await openSeededSession(page) + await page.getByText(FIXTURE.markers.assistant(FIXTURE.turns), { exact: false }).last() + .waitFor({ timeout: 30_000 }) + }, 180_000) + + afterAll(async () => { + await browser?.close() + await scaffold?.close() + }) + + it('takes presses above and beside the label', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-conversation-view-tabs-target')) + const chat = await probeTab(page, 'Chat') + expect(chat.button.top).toBeLessThan(chat.label.top - 8) + expect(chat.button.left).toBeLessThan(chat.label.left - 8) + expect(chat.ownsAboveLabel).toBe(true) + expect(chat.ownsBesideLabel).toBe(true) + expect(tripwire.pageErrors).toEqual([]) + }) + + it('keeps the selected bar above the Trajectory toolbar', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-conversation-view-tabs-trajectory')) + await page.getByRole('tab', { name: 'Trajectory', exact: true }).click() + const toolbar = page.getByRole('toolbar', { name: 'Trajectory toolbar' }) + await toolbar.waitFor({ timeout: 30_000 }) + const trajectory = await probeTab(page, 'Trajectory') + // The toolbar must start within the bar's overhang, or the probe cannot fail. + expect((await toolbar.boundingBox())!.y).toBeLessThanOrEqual(trajectory.button.bottom + 1) + expect(trajectory.ownsBarOverhang).toBe(true) + expect(tripwire.pageErrors).toEqual([]) + }) +}) diff --git a/apps/web/tests/desktop-locale.e2e.ts b/apps/web/tests/desktop-locale.e2e.ts index cc43d697f1..4ede5dee36 100644 --- a/apps/web/tests/desktop-locale.e2e.ts +++ b/apps/web/tests/desktop-locale.e2e.ts @@ -21,7 +21,7 @@ describe.skipIf(MODE === 'record')('web e2e: native and Client locale preference let page: Page let tripwire: ReturnType const reported: string[] = [] - let languages = ['fr-FR', 'zh-CN', 'en-US'] + let languages = ['it-IT', 'zh-CN', 'en-US'] beforeAll(async () => { scaffold = await launchWebScaffold() diff --git a/apps/web/tests/entry-customization.e2e.ts b/apps/web/tests/entry-customization.e2e.ts index 4ea12f0238..8161012d28 100644 --- a/apps/web/tests/entry-customization.e2e.ts +++ b/apps/web/tests/entry-customization.e2e.ts @@ -25,37 +25,33 @@ describe('default navigation entry customization', () => { try { await browser?.close() } finally { await scaffold?.close() } }) - it('unpins a sidebar panel across reloads while its panel stays reachable from More', async () => { + it('keeps navigation outside More and offers only browsing and creation after reload', async () => { onTestFailed(() => saveFailureShot(page, 'entry-customization')) const nav = page.getByRole('navigation', { name: 'Global panels' }) - const plugins = nav.getByRole('button', { name: 'Plugins', exact: true }) const tasks = nav.getByRole('button', { name: 'Automation tasks', exact: true }) const more = nav.getByRole('button', { name: 'More', exact: true }) - await plugins.waitFor() await tasks.waitFor() - await more.waitFor() - const menu = page.getByRole('menu') - - await more.click() - expect(await menu.getByRole('menuitem', { name: 'Plugins', exact: true }).count()).toBe(0) - await menu.getByRole('menuitem', { name: 'Unpin from left sidebar: Automation tasks', exact: true }).click() - await tasks.waitFor({ state: 'detached' }) - await menu.getByRole('menuitem', { name: 'Automation tasks', exact: true }).click() + await tasks.click() await page.getByRole('heading', { name: 'Automation tasks', exact: true }).waitFor() - // The selected panel keeps its row until another panel is selected. - await tasks.waitFor() - expect(await tasks.getAttribute('aria-current')).toBe('page') - await plugins.click() - await tasks.waitFor({ state: 'detached' }) - expect(await plugins.getAttribute('aria-current')).toBe('page') - - await page.reload({ waitUntil: 'load' }) - await plugins.waitFor() - await more.waitFor() - expect(await tasks.count()).toBe(0) - await more.click() - await menu.getByRole('menuitem', { name: 'Pin to left sidebar: Automation tasks', exact: true }).click() - await tasks.waitFor() + for (const reload of [false, true]) { + if (reload) { + await page.reload({ waitUntil: 'load' }) + await tasks.click() + await page.getByRole('heading', { name: 'Automation tasks', exact: true }).waitFor() + } + await tasks.waitFor() + await more.click() + const menu = page.getByRole('menu') + await menu.getByRole('menuitem', { name: 'Browse other plugins', exact: true }).waitFor() + expect(await menu.getByRole('menuitem').allTextContents()).toEqual(['Browse other plugins', 'Create your plugin']) + expect(await menu.getByRole('separator').count()).toBe(0) + expect(await tasks.getAttribute('aria-current')).toBe('page') + await menu.getByRole('menuitem', { name: 'Browse other plugins', exact: true }).click() + await menu.waitFor({ state: 'detached' }) + expect(await nav.getByRole('button', { name: 'Plugins', exact: true }).getAttribute('aria-current')).toBe('page') + await tasks.click() + await page.getByRole('heading', { name: 'Automation tasks', exact: true }).waitFor() + } expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) }) diff --git a/apps/web/tests/expected/additional-locales/de.expected.md b/apps/web/tests/expected/additional-locales/de.expected.md new file mode 100644 index 0000000000..210ffed2fb --- /dev/null +++ b/apps/web/tests/expected/additional-locales/de.expected.md @@ -0,0 +1,5 @@ +- listitem: + - button "Siehe Automatische Autorisierungsüberprüfung": Automatische Autorisierungsüberprüfung + - text: Experimentell Fügen Sie einen Auto review-Berechtigungsmodus hinzu, der das Modell verwendet, um die Autorisierung vor jedem Toolaufruf zu bewerten. + - status + - switch "Aktivieren Sie Automatische Autorisierungsüberprüfung" diff --git a/apps/web/tests/expected/additional-locales/es.expected.md b/apps/web/tests/expected/additional-locales/es.expected.md new file mode 100644 index 0000000000..c1e0233d70 --- /dev/null +++ b/apps/web/tests/expected/additional-locales/es.expected.md @@ -0,0 +1,5 @@ +- listitem: + - button "Ver Revisión de autorización automática": Revisión de autorización automática + - text: Experimental Agregue un modo de permiso Auto review que utilice el modelo para evaluar la autorización antes de cada llamada a la herramienta. + - status + - switch "Habilitar Revisión de autorización automática" diff --git a/apps/web/tests/expected/additional-locales/fr.expected.md b/apps/web/tests/expected/additional-locales/fr.expected.md new file mode 100644 index 0000000000..88c6dad142 --- /dev/null +++ b/apps/web/tests/expected/additional-locales/fr.expected.md @@ -0,0 +1,5 @@ +- listitem: + - button "Voir Examen des autorisations automatiques": Examen des autorisations automatiques + - text: Expérimental Ajoutez un mode d'autorisation Auto review qui utilise le modèle pour évaluer l'autorisation avant chaque appel d'outil. + - status + - switch "Activer Examen des autorisations automatiques" diff --git a/apps/web/tests/expected/additional-locales/pt.expected.md b/apps/web/tests/expected/additional-locales/pt.expected.md new file mode 100644 index 0000000000..1119ad2aa1 --- /dev/null +++ b/apps/web/tests/expected/additional-locales/pt.expected.md @@ -0,0 +1,5 @@ +- listitem: + - button "Ver Revisão de autorização automática": Revisão de autorização automática + - text: Experimental Adicione um modo de permissão Auto review que usa o modelo para avaliar a autorização antes de cada chamada de ferramenta. + - status + - switch "Ativar Revisão de autorização automática" diff --git a/apps/web/tests/expected/additional-locales/ru.expected.md b/apps/web/tests/expected/additional-locales/ru.expected.md new file mode 100644 index 0000000000..e4f02c9c69 --- /dev/null +++ b/apps/web/tests/expected/additional-locales/ru.expected.md @@ -0,0 +1,5 @@ +- listitem: + - button "Посмотреть Проверка автоматической авторизации": Проверка автоматической авторизации + - text: Экспериментальный Добавьте режим разрешений Auto review, который использует модель для оценки авторизации перед каждым вызовом инструмента. + - status + - switch "Включить Проверка автоматической авторизации" diff --git a/apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md b/apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md index 6ea83d310a..bde82cec8f 100644 --- a/apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md +++ b/apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md @@ -37,7 +37,7 @@ - text: 上下文窗口 - textbox "上下文窗口 1": - /placeholder: 1M - - text: 1M + - text: "1048576" - text: 最大输出 token 数 - textbox "最大输出 token 数 1": - /placeholder: 256K diff --git a/apps/web/tests/expected/plugin-config/official.expected.md b/apps/web/tests/expected/plugin-config/official.expected.md index c3f1cafc28..826732727e 100644 --- a/apps/web/tests/expected/plugin-config/official.expected.md +++ b/apps/web/tests/expected/plugin-config/official.expected.md @@ -33,10 +33,10 @@ - status - switch "启用 语音输入" - listitem: - - button "查看 自定义插件市场": 自定义插件市场 - - text: 实验性 在插件页添加「发现」标签页,从你添加的插件市场索引浏览和安装插件。 + - button "查看 自定义插件索引": 自定义插件索引 + - text: 实验性 在插件页添加「发现」标签页,从你添加的插件索引浏览和安装插件。 - status - - switch "启用 自定义插件市场" + - switch "启用 自定义插件索引" - listitem: - button "查看 自动授权审查": 自动授权审查 - text: 实验性 提供自动审查权限模式,由模型在每次工具调用前判断是否授权。 diff --git a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md index fd91449871..dfdde9ceb5 100644 --- a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md +++ b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md @@ -33,10 +33,10 @@ - status - switch "启用 语音输入" - listitem: - - button "查看 自定义插件市场": 自定义插件市场 - - text: 实验性 在插件页添加「发现」标签页,从你添加的插件市场索引浏览和安装插件。 + - button "查看 自定义插件索引": 自定义插件索引 + - text: 实验性 在插件页添加「发现」标签页,从你添加的插件索引浏览和安装插件。 - status - - switch "启用 自定义插件市场" + - switch "启用 自定义插件索引" - listitem: - button "查看 自动授权审查": 自动授权审查 - text: 实验性 提供自动审查权限模式,由模型在每次工具调用前判断是否授权。 diff --git a/apps/web/tests/expected/plugin-manager/manager.expected.md b/apps/web/tests/expected/plugin-manager/manager.expected.md index 730b82532f..cd2e2ac168 100644 --- a/apps/web/tests/expected/plugin-manager/manager.expected.md +++ b/apps/web/tests/expected/plugin-manager/manager.expected.md @@ -33,10 +33,10 @@ - status - switch "启用 语音输入" - listitem: - - button "查看 自定义插件市场": 自定义插件市场 - - text: 实验性 在插件页添加「发现」标签页,从你添加的插件市场索引浏览和安装插件。 + - button "查看 自定义插件索引": 自定义插件索引 + - text: 实验性 在插件页添加「发现」标签页,从你添加的插件索引浏览和安装插件。 - status - - switch "启用 自定义插件市场" + - switch "启用 自定义插件索引" - listitem: - button "查看 自动授权审查": 自动授权审查 - text: 实验性 提供自动审查权限模式,由模型在每次工具调用前判断是否授权。 diff --git a/apps/web/tests/permission-policy-context.e2e.ts b/apps/web/tests/permission-policy-context.e2e.ts index c57ee84667..0490e0cb89 100644 --- a/apps/web/tests/permission-policy-context.e2e.ts +++ b/apps/web/tests/permission-policy-context.e2e.ts @@ -183,13 +183,13 @@ describe('web e2e: current sandbox policy reaches the model before tools', () => expect(contexts).toHaveLength(4) expect(contexts[0]).toContain('Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode.') expect(contexts[0]).toContain('Do not refuse a required modification from this policy alone') - expect(contexts[0]).toContain('Approval policy: ask.') + expect(contexts[0]).toContain('Approval prompts are enabled in this session') expect(contexts[1]).toContain('Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.') expect(contexts[1]).toContain('Approval prompts are disabled in this session') if (sessionWorkspace === undefined) throw new Error('permission-policy scenario observed no session workspace') expect(contexts[2]).toContain(`Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: ${JSON.stringify(canonicalPath(sessionWorkspace))}. Some platform temporary areas may also be writable.`) - expect(contexts[2]).toContain('Approval policy: ask.') + expect(contexts[2]).toContain('Approval prompts are enabled in this session') expect(contexts[2]).not.toContain('Approval prompts are disabled in this session') expect(contexts[3]).toContain('Current DSH file policy: read-only.') diff --git a/apps/web/tests/plugin-manager.e2e.ts b/apps/web/tests/plugin-manager.e2e.ts index a6df6faab4..a85297938f 100644 --- a/apps/web/tests/plugin-manager.e2e.ts +++ b/apps/web/tests/plugin-manager.e2e.ts @@ -837,7 +837,7 @@ describe('web e2e: plugin manager', () => { expect(await panel.locator('[data-plugin-group="basic"] [data-plugin-item]').count()).toBe(4) expect(await panel.locator('[data-plugin-package="@deepseek-ai/dsh-experimental-inspector"]').count()).toBe(0) expect(await panel.getByRole('switch', { name: '启用 语音输入', exact: true }).getAttribute('aria-checked')).toBe('false') - expect(await panel.getByRole('switch', { name: '启用 自定义插件市场', exact: true }).getAttribute('aria-checked')).toBe('false') + expect(await panel.getByRole('switch', { name: '启用 自定义插件索引', exact: true }).getAttribute('aria-checked')).toBe('false') expect(await panel.getByRole('switch', { name: '启用 开发者工具', exact: true }).count()).toBe(0) expect(await panel.getByText(/Cordis|Chrome DevTools/).count()).toBe(0) await panel.getByRole('button', { name: '更多', exact: true }).click() diff --git a/apps/web/tests/schedule-after.e2e.ts b/apps/web/tests/schedule-after.e2e.ts index 1fc1ef2395..b400dbecd9 100644 --- a/apps/web/tests/schedule-after.e2e.ts +++ b/apps/web/tests/schedule-after.e2e.ts @@ -20,6 +20,7 @@ import type { ContextFormed, GenerateOptions, StreamChunk } from '@deepseek-ai/d import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session' import { ScheduleId, + createAfterScheduleRecord, createEveryScheduleRecord, foldScheduleEvents, type EveryScheduleRecord, @@ -1259,4 +1260,86 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => { await page.clock.setFixedTime(new Date(CATALOG_NOW)) } }, 60_000) + + it('removes every ended task from the page and keeps the active ones', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-schedule-clear-ended')) + const endedSession = SessionId('schedule-manager-ended-clear') + const domain = scaffold.ctx.storageDomain.get('schedule') + if (domain === undefined) throw new Error('Schedule domain was not opened') + const endedIds = [ScheduleId('schedule-ended-first'), ScheduleId('schedule-ended-second')] + const endedTitles = ['Ended release check', 'Ended metrics check'] as const + // An earlier scenario may leave an ended row behind; the confirmation states + // the whole ended set, so the lane starts from an empty one. + await scaffold.ctx.schedule.deleteInactive() + for (const [index, id] of endedIds.entries()) { + const title = endedTitles[index]! + const record = createAfterScheduleRecord(id, title, 60, Date.now(), title) + const receipt = { + scheduledAt: record.scheduledAt, + deliveredAt: new Date(Date.parse(record.scheduledAt) + 1_000).toISOString(), + messageId: MessageId(`${id}-delivery`), + } + await domain.table('tasks').put(record.id, { + sessionId: endedSession, record, status: 'inactive', lastDelivery: receipt, + deliveryHistory: { records: [receipt], earlierRecordsUnavailable: false }, + }) + } + // The running task is what the cleanup must leave alone. Creating it also + // announces a committed Host change, so the open page reads the rows seeded + // above without waiting for its own first catalog read. + const kept = await scaffold.ctx.schedule.create(endedSession, { + prompt: 'Keep this running task', title: 'Keep this running task', at: '2099-08-25T12:08:00.000Z', + }) + try { + await page.getByRole('button', { name: 'Automation tasks', exact: true }).click() + const manager = page.getByTestId('task-manager-page') + // Earlier scenarios leave their own search text and status filter on this + // shared page; this scenario asserts the complete list. + await manager.getByRole('searchbox', { name: 'Search tasks' }).fill('') + await manager.getByRole('button', { name: 'All', exact: true }).click() + const more = manager.getByRole('button', { name: 'More task actions', exact: true }) + await more.waitFor({ timeout: 15_000 }) + + // The confirmation states the Host-side target and removes nothing until it + // is confirmed. + await more.click() + await page.getByRole('menuitem', { name: 'Clear all inactive tasks', exact: true }).click() + const confirmation = page.getByRole('dialog', { name: 'Clear 2 inactive tasks?' }) + await confirmation.getByRole('button', { name: 'Cancel', exact: true }).click() + expect((await scaffold.ctx.schedule.catalog()).filter(task => task.status === 'inactive')).toHaveLength(2) + + await more.click() + await page.getByRole('menuitem', { name: 'Clear all inactive tasks', exact: true }).click() + await confirmation.getByRole('button', { name: 'Clear tasks', exact: true }).click() + await expect.poll( + async () => (await scaffold.ctx.schedule.catalog()).filter(task => task.status === 'inactive'), + { timeout: 15_000 }, + ).toEqual([]) + const remaining = await scaffold.ctx.schedule.catalog() + expect(remaining.some(task => endedIds.includes(task.id))).toBe(false) + expect(remaining.map(task => task.title)).toContain('Keep this running task') + await page.getByText('Cleared 2 inactive tasks.', { exact: true }).waitFor({ timeout: 15_000 }) + // The trigger left with the rows it removed, so the page heading owns the + // focus after the confirmation closes; this confirms the heading fallback + // in a real browser. + await expect.poll(() => page.evaluate(() => document.activeElement?.tagName), { timeout: 15_000 }) + .toBe('H1') + // The page follows the acknowledged removal: no ended row or cleanup action + // remains, and the running task is still listed. + await expect.poll(() => manager.getByRole('button', { name: 'More task actions', exact: true }).count(), { timeout: 15_000 }) + .toBe(0) + await manager.getByRole('button', { name: 'All', exact: true }).click() + await expect.poll(() => manager.getByRole('list', { name: 'Task catalog' }) + .getByRole('button', { name: kept.title, exact: true }).count(), { timeout: 15_000 }).toBe(1) + expect((await scaffold.ctx.schedule.catalog()).filter(task => task.status === 'inactive')).toEqual([]) + expect(tripwire.pageErrors).toEqual([]) + } finally { + // The shared page and Host outlive a failed assertion: remove every row + // this scenario introduced so the next one starts from the same state. A + // teardown rejection must not mask the assertion that failed first. + await Promise.allSettled( + [kept.id, ...endedIds].map(id => scaffold.ctx.schedule.delete({ sessionId: endedSession, id })), + ) + } + }, 60_000) }) diff --git a/apps/web/tests/settings-chrome.e2e.ts b/apps/web/tests/settings-chrome.e2e.ts index 4110b72e95..a3ecc0de18 100644 --- a/apps/web/tests/settings-chrome.e2e.ts +++ b/apps/web/tests/settings-chrome.e2e.ts @@ -977,18 +977,18 @@ describe('web e2e: settings modal and General preferences', () => { it('opens a browser asking for no shipped language in English', async () => { // The product default for "no usable signal": a French browser ships - // neither zh nor en, so resolution falls to FALLBACK_LOCALE (en) rather + // names no shipped language, so resolution falls to FALLBACK_LOCALE (en) rather // than to Chinese. const fresh = await launchWebScaffold({ developerTools: false }) - const frPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'fr-FR' }) - const frTripwire = watchConsole(frPage) - onTestFailed(() => saveSettingsFailureShot(frPage, 'settings-unshipped-language')) + const unsupportedPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'it-IT' }) + const unsupportedTripwire = watchConsole(unsupportedPage) + onTestFailed(() => saveSettingsFailureShot(unsupportedPage, 'settings-unshipped-language')) try { - await frPage.goto(fresh.authenticatedUrl, { waitUntil: 'load' }) - await frPage.waitForSelector('[class*="frame"]', { timeout: 30_000 }) - expect(await frPage.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull() - await openSettings(frPage, 'en') - const dialog = frPage.getByRole('dialog', { name: 'Settings' }) + await unsupportedPage.goto(fresh.authenticatedUrl, { waitUntil: 'load' }) + await unsupportedPage.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + expect(await unsupportedPage.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull() + await openSettings(unsupportedPage, 'en') + const dialog = unsupportedPage.getByRole('dialog', { name: 'Settings' }) await dialog.waitFor({ timeout: 10_000 }) await dialog.getByRole('button', { name: 'English' }).waitFor({ timeout: 10_000 }) // A locale-owned nav label proves the dictionaries resolved to en. @@ -996,17 +996,17 @@ describe('web e2e: settings modal and General preferences', () => { // The markup already ships `en`, so this alone cannot prove the sync ran // — the zh scenario above is the discriminating half. Asserted here too // so a future change that resolves en but writes the wrong tag is caught. - expect(await frPage.evaluate(() => document.documentElement.lang)).toBe('en') + expect(await unsupportedPage.evaluate(() => document.documentElement.lang)).toBe('en') // Golden of the English fallback dialog — the visible output this change // produces. The zh golden above covers the detected-locale surface, so // the pair pins both directions of the resolution. await dialog.getByText(`Current version: ${version}`, { exact: true }).waitFor() - const snapshot = await captureStableAria(frPage, '[role="dialog"]', fresh.workspaceCwd, versionCapture) + const snapshot = await captureStableAria(unsupportedPage, '[role="dialog"]', fresh.workspaceCwd, versionCapture) await compareOrRefreshGolden(DIALOG_EN_EXPECTED, snapshot, MODE) - expect(frTripwire.pageErrors).toEqual([]) - expect(frTripwire.warnings).toEqual([]) + expect(unsupportedTripwire.pageErrors).toEqual([]) + expect(unsupportedTripwire.warnings).toEqual([]) } finally { - await frPage.close() + await unsupportedPage.close() await fresh.close() } }, 90_000) diff --git a/apps/web/tests/stats-truncated-tooltip.e2e.ts b/apps/web/tests/stats-truncated-tooltip.e2e.ts new file mode 100644 index 0000000000..18e7d5e64c --- /dev/null +++ b/apps/web/tests/stats-truncated-tooltip.e2e.ts @@ -0,0 +1,108 @@ +// Web e2e scenario: a composer statistics pill cut off by the narrow composer +// row shows its full reading in a tooltip on hover. The seed is generated, not recorded: no line is model output. +import type { Browser, Page } from 'playwright' +import { chromium } from 'playwright' +import { createSystemMessage } from '@deepseek-ai/dsh-llm' +import { SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts' +import { newEnglishPage, saveFailureShot } from './support.ts' + +const SEED_ID = 'stats-truncated-tooltip-web-e2e' +const USAGE_READING = '5.8M tok · Cache hit 98%' + +/** + * Generate one closed single-step turn whose billed usage totals 5.8M tokens at a 98% cache hit. + * @returns session.jsonl text for {@link seedSession}. + */ +function buildSeed(): string { + const lines = [JSON.stringify({ + type: 'session', version: SESSION_FORMAT_VERSION, id: '{{sessionId}}', + createdAt: 1784974100000, cwd: '{{cwd}}/workspace', isSeeded: false, delegationDepth: 0, + })] + let seq = 0 + let time = 1784974100000 + const at = (event: Record): void => { + lines.push(JSON.stringify({ ...event, seq: seq++, time: time++ })) + } + at({ type: 'turn/start', data: { turn: 1 } }) + at({ type: 'step/start', data: { turn: 1, step: 1 } }) + at({ type: 'system/message', data: { turn: 1, step: 1, message: createSystemMessage('') }, surfaceOp: 'append' }) + at({ + type: 'user/message', + data: { + id: '00000000-0000-4000-9000-000000000001', + role: 'user', + content: [{ type: 'text', text: 'm1' }], + source: { kind: 'user' }, + }, + surfaceOp: 'append', + }) + at({ + type: 'assistant/message', + data: { + stream: [], + turn: 1, + step: 1, + message: { + id: '00000000-0000-4000-8000-000000000001', + role: 'assistant', + content: [{ type: 'text', text: 'r1' }], + source: { kind: 'model', provider: 'snapshot', model: 'snapshot-replier' }, + }, + usage: { inputTokens: 100_000, outputTokens: 20_000, cacheReadTokens: 5_680_000 }, + }, + surfaceOp: 'append', + }) + at({ type: 'step/end', data: { turn: 1, step: 1 } }) + at({ type: 'turn/end', data: { turn: 1, reason: { kind: 'completed' } } }) + return `${lines.join('\n')}\n` +} + +describe('web e2e: truncated composer statistics', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + + beforeAll(async () => { + if (webSnapshotMode() === 'record') throw new Error('stats-truncated-tooltip is a keyless assembled scenario') + scaffold = await launchWebScaffold({}) + await seedSession(scaffold, buildSeed(), SEED_ID) + browser = await chromium.launch() + page = await newEnglishPage(browser) + tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + }, 120_000) + + afterAll(async () => { + await browser?.close() + await scaffold?.close() + }) + + it('shows the full reading of a pill the narrow composer cuts off', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-stats-truncated-tooltip')) + await page.locator('[role="treeitem"]').first().click() + await page.locator('[role="treeitem"]').nth(1).click() + const usagePill = page.getByRole('button', { name: USAGE_READING, exact: true }) + await usagePill.waitFor({ timeout: 15_000 }) + const tooltip = page.getByRole('tooltip') + const label = usagePill.locator(':scope > span') + const cut = () => label.evaluate(element => element.scrollWidth > element.clientWidth) + expect(await cut()).toBe(false) + + await page.setViewportSize({ width: 390, height: 900 }) + await expect.poll(cut).toBe(true) + await usagePill.hover() + await expect.poll(() => tooltip.textContent()).toBe(USAGE_READING) + await usagePill.click() + await page.getByRole('dialog', { name: 'Token usage' }).waitFor() + expect(await tooltip.count()).toBe(0) + }, 60_000) + + it('stayed clean', () => { + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }) +}) diff --git a/apps/web/tests/trajectory-image-display.expected.e2e.ts b/apps/web/tests/trajectory-image-display.expected.e2e.ts index 2a3ffc9aca..b8da2ed2c0 100644 --- a/apps/web/tests/trajectory-image-display.expected.e2e.ts +++ b/apps/web/tests/trajectory-image-display.expected.e2e.ts @@ -77,6 +77,7 @@ it('renders durable record images in the Trajectory details panel from the share // The virtual ledger needs a measurable viewport; jsdom reports zero // heights, so pin one and neutralize the imperative tail scroll. vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(600) + vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(1000) Object.defineProperty(HTMLElement.prototype, 'scrollTo', { configurable: true, value: () => {}, diff --git a/apps/web/tests/window-drag-coverage.e2e.ts b/apps/web/tests/window-drag-coverage.e2e.ts index b7ff2b15df..cdbb727736 100644 --- a/apps/web/tests/window-drag-coverage.e2e.ts +++ b/apps/web/tests/window-drag-coverage.e2e.ts @@ -374,7 +374,9 @@ describe('web e2e: macOS window drag coverage', () => { { row: 'conversation header', what: 'the blank run right of the view tabs, on the tab strip row', - at: () => [tabs.x + tabs.width + 8, tabs.y + tabs.height / 2], + // The last tab's press target overhangs the tablist by 10px and the + // More launcher starts 20px past it; probe the run between them. + at: () => [tabs.x + tabs.width + 15, tabs.y + tabs.height / 2], drag: true, }, { diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 057a50fbea..305f54b5f2 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -24,6 +24,7 @@ // cannot see both sides of the cordis Context merges). "exclude": [ "tests/japanese-plugin-metadata.e2e.ts", + "tests/additional-locales.e2e.ts", "tests/rich-text-quotes.e2e.ts", "tests/hover-input.e2e.ts", "tests/collapse-timing.e2e.ts", @@ -149,6 +150,7 @@ "tests/preview-boot.e2e.ts", "tests/seeded-history.e2e.ts", "tests/stats-paged-history.e2e.ts", + "tests/stats-truncated-tooltip.e2e.ts", "tests/sidebar-scrollbar.e2e.ts", "tests/sidebar-title-hover-scroll.e2e.ts", "tests/rail-search-expand.e2e.ts", @@ -222,6 +224,7 @@ "tests/chat-long-interactions.e2e.ts", "tests/chat-continuous-conversation.e2e.ts", "tests/composer-tab-geometry.e2e.ts", + "tests/conversation-view-tabs.e2e.ts", "tests/window-drag-coverage.e2e.ts", "tests/complex-history.perf.ts", "tests/pwsh-terminal.e2e.ts", diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 76fb882b6d..00850d7b3f 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1791,25 +1791,15 @@ export interface Config { ## `@deepseek-ai/dsh-llm-deepseek-account` - `inject`: `llm` -- `refs`: [`DeepSeekCatalogModel`](../packages/llm/llm-deepseek/src/index.ts) · [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/llm/llm-deepseek-account/src/config.ts:19`](../packages/llm/llm-deepseek-account/src/config.ts) +- `refs`: [`ModelListingConfig`](../packages/llm/llm-deepseek/src/index.ts) · [`ModelOverride`](../packages/llm/llm-deepseek/src/index.ts) · [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) +- `source`: [`packages/llm/llm-deepseek-account/src/config.ts:15`](../packages/llm/llm-deepseek-account/src/config.ts) ```ts config-catalog /** Account route configuration; authentication comes exclusively from the account service. */ -export interface Config extends Omit { - /** Deadline of one listing request, which also bounds a first-use wait (default five seconds). */ - modelsTimeoutMs: Volatile - /** - * Listing age after which the next access refreshes it in the background; - * a failed load retries after the same age (default ten minutes). - */ - modelsMaxAgeMs: Volatile +export interface Config extends Omit, ModelListingConfig { /** Field overrides by model id, layered over the account listing and the built-in catalog. */ modelOverrides: Volatile> } - -/** Field overrides for one model; unset fields and empty lists inherit the underlying entry. */ -export type ModelOverride = Omit ``` @@ -1819,12 +1809,12 @@ export type ModelOverride = Omit ## `@deepseek-ai/dsh-llm-deepseek-api-key` - `inject`: `llm` -- `refs`: [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/llm/llm-deepseek-api-key/src/config.ts:10`](../packages/llm/llm-deepseek-api-key/src/config.ts) +- `refs`: [`ModelListingConfig`](../packages/llm/llm-deepseek/src/index.ts) · [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) +- `source`: [`packages/llm/llm-deepseek-api-key/src/config.ts:18`](../packages/llm/llm-deepseek-api-key/src/config.ts) ```ts config-catalog /** Messages configuration with a per-request API-key reference. */ -export interface Config extends ProtocolConfig { +export interface Config extends ProtocolConfig, ModelListingConfig { /** Credential reference resolved per request; defaults to DEEPSEEK_API_KEY. */ apiKeyEnv: Volatile } @@ -4371,7 +4361,7 @@ export interface Config { ## `@deepseek-ai/dsh-user-approval` - `inject`: `sessionProjections` -- `source`: [`packages/interaction/user-approval/src/index.ts:147`](../packages/interaction/user-approval/src/index.ts) +- `source`: [`packages/interaction/user-approval/src/index.ts:152`](../packages/interaction/user-approval/src/index.ts) ```ts config-catalog /** Plugin config. All optional — `static Config` supplies the defaults. */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 931142ba57..9d14048d98 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -1793,25 +1793,15 @@ export interface Config { ## `@deepseek-ai/dsh-llm-deepseek-account` - `inject`: `llm` -- `refs`: [`DeepSeekCatalogModel`](../packages/llm/llm-deepseek/src/index.ts) · [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/llm/llm-deepseek-account/src/config.ts:19`](../packages/llm/llm-deepseek-account/src/config.ts) +- `refs`: [`ModelListingConfig`](../packages/llm/llm-deepseek/src/index.ts) · [`ModelOverride`](../packages/llm/llm-deepseek/src/index.ts) · [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) +- `source`: [`packages/llm/llm-deepseek-account/src/config.ts:15`](../packages/llm/llm-deepseek-account/src/config.ts) ```ts config-catalog /** Account route configuration; authentication comes exclusively from the account service. */ -export interface Config extends Omit { - /** Deadline of one listing request, which also bounds a first-use wait (default five seconds). */ - modelsTimeoutMs: Volatile - /** - * Listing age after which the next access refreshes it in the background; - * a failed load retries after the same age (default ten minutes). - */ - modelsMaxAgeMs: Volatile +export interface Config extends Omit, ModelListingConfig { /** Field overrides by model id, layered over the account listing and the built-in catalog. */ modelOverrides: Volatile> } - -/** Field overrides for one model; unset fields and empty lists inherit the underlying entry. */ -export type ModelOverride = Omit ``` @@ -1821,12 +1811,12 @@ export type ModelOverride = Omit ## `@deepseek-ai/dsh-llm-deepseek-api-key` - `inject`: `llm` -- `refs`: [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/llm/llm-deepseek-api-key/src/config.ts:10`](../packages/llm/llm-deepseek-api-key/src/config.ts) +- `refs`: [`ModelListingConfig`](../packages/llm/llm-deepseek/src/index.ts) · [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`) +- `source`: [`packages/llm/llm-deepseek-api-key/src/config.ts:18`](../packages/llm/llm-deepseek-api-key/src/config.ts) ```ts config-catalog /** Messages configuration with a per-request API-key reference. */ -export interface Config extends ProtocolConfig { +export interface Config extends ProtocolConfig, ModelListingConfig { /** Credential reference resolved per request; defaults to DEEPSEEK_API_KEY. */ apiKeyEnv: Volatile } @@ -4373,7 +4363,7 @@ export interface Config { ## `@deepseek-ai/dsh-user-approval` - `inject`: `sessionProjections` -- `source`: [`packages/interaction/user-approval/src/index.ts:147`](../packages/interaction/user-approval/src/index.ts) +- `source`: [`packages/interaction/user-approval/src/index.ts:152`](../packages/interaction/user-approval/src/index.ts) ```ts config-catalog /** Plugin config. All optional — `static Config` supplies the defaults. */ diff --git a/docs/cookbook/adding-a-package.i18n.yaml b/docs/cookbook/adding-a-package.i18n.yaml index 2f95205489..28ee60cb3d 100644 --- a/docs/cookbook/adding-a-package.i18n.yaml +++ b/docs/cookbook/adding-a-package.i18n.yaml @@ -21,8 +21,8 @@ en: 0894bd9f35430753 zh: 2d28090e71134edd /cookbook-adding-a-workspace-package/5-add-optional-plugin-display-metadata: - en: 1238eaea376391d1 - zh: c7e606f95d8980bf + en: ee71fb0a70f02550 + zh: 1f0d6936d6c2acad /cookbook-adding-a-workspace-package/6-verify: en: 80c7fb90b1175aae zh: 2ba37962dbc57299 diff --git a/docs/cookbook/adding-a-package.md b/docs/cookbook/adding-a-package.md index 2950932479..16fe3e564f 100644 --- a/docs/cookbook/adding-a-package.md +++ b/docs/cookbook/adding-a-package.md @@ -136,7 +136,7 @@ Merge these entries into `package.json`, retaining existing runtime exports and } ``` -Keep language files together in `locale/`, with `en.json` as the discovery entry. Fields are optional; present values must be non-empty strings. English `meta.title` and `meta.description` use sentence case: capitalize only the first word, proper nouns, and product names, as in `Git worktrees` ([rule](../../.agents/skills/dsh-prose-standard/SKILL.md#required-coverage-by-prose-location)). At runtime, missing files or fields permit fallback, while malformed JSON or invalid fields produce a per-plugin diagnostic. First-party packages and plugin templates must provide `ja.json` covering every field present in `en.json` or optional `zh.json`, preserving placeholders; detail paths may point to the existing English document. Subpath resources under `locale//` follow the same checks. English descriptions explain user-visible behavior and end with sentence punctuation. Run `pnpm run check:i18n` to verify JSON metadata and UI dictionary completeness; the JSON checks also run in `check:all`, CI static checks, and hygiene. +Keep language files together in `locale/`, with `en.json` as the discovery entry. Fields are optional; present values must be non-empty strings. English `meta.title` and `meta.description` use sentence case: capitalize only the first word, proper nouns, and product names, as in `Git worktrees` ([rule](../../.agents/skills/dsh-prose-standard/SKILL.md#required-coverage-by-prose-location)). At runtime, missing files or fields permit fallback, while malformed JSON or invalid fields produce a per-plugin diagnostic. First-party packages and plugin templates must provide `ja.json`, `es.json`, `fr.json`, `de.json`, `ru.json`, and `pt.json` covering every field present in `en.json` or optional `zh.json`, preserving placeholders, slash commands, and API field names; detail paths may point to the existing English document. Subpath resources under `locale//` follow the same checks. English descriptions explain user-visible behavior and end with sentence punctuation. Run `pnpm run check:i18n` to verify JSON metadata and UI dictionary completeness; the JSON checks also run in `check:all`, CI static checks, and hygiene. Fields fall back independently before view-specific name formatting, using the existing locale language chain first: @@ -147,7 +147,7 @@ Export `/locale/en.json` for locale lookup; expose ` For an image on bundle cards, details, and component rows, a package-root plugin sets top-level `"icon": "./icon.svg"` in its exported manifest, or exports `./icon` when that field is omitted. A subpath plugin exports its own icon, such as `"./search/icon": "./search.svg"`. Include each image in `files`. A manifest `icon` is relative to the manifest's directory; absolute paths, URLs, paths outside that directory, and symlinks resolving outside it are rejected. An exported icon resolves through Node exports like locale resources and must remain inside its package. SVG, PNG, JPEG (`.jpg`/`.jpeg`), and WebP files are supported up to 256 KiB. Images must be self-contained; SVG is rendered as an image, not inline HTML. The Host returns a data URL without activating the plugin. Invalid declarations or unreadable files produce a metadata diagnostic while retaining valid text, without trying the export; missing or undecodable images use the panel's default artwork. -A bundle can also set `meta.detail` to a Markdown file path relative to the package root, such as `"docs/DETAIL.md"` in `locale/en.json` and `"docs/DETAIL.zh.md"` in `locale/zh.json`; it is read from these files at the package root. The path uses `/`, stays inside the package, ends in `.md` or `.markdown`, and has no package-field fallback; keep its images beside it, and list its top-level directory, such as `docs`, in `files` right after the locale resources, as `check-workspace-constraints` requires. The named file must exist in a subdirectory of the package; a root-level document is rejected because its neighboring images would not be published. A bundle's page in **Plugins**, and its market entry page when the [custom plugin markets bundle](../../packages/experimental/plugin-market-bundle/README.md) is on, show the file for the active UI language as the plugin's detail document. Without `meta.detail` they show the package README, and a market entry page then falls back to the declared repository's README; the [document order](../../packages/util/plugin-detail/README.md#document-order) defines the rules. Write it for users, and reference images by paths relative to the file. +A bundle can also set `meta.detail` to a Markdown file path relative to the package root, such as `"docs/DETAIL.md"` in `locale/en.json` and `"docs/DETAIL.zh.md"` in `locale/zh.json`; it is read from these files at the package root. The path uses `/`, stays inside the package, ends in `.md` or `.markdown`, and has no package-field fallback; keep its images beside it, and list its top-level directory, such as `docs`, in `files` right after the locale resources, as `check-workspace-constraints` requires. The named file must exist in a subdirectory of the package; a root-level document is rejected because its neighboring images would not be published. A bundle's page in **Plugins**, and its index entry page when the [custom plugin indexes bundle](../../packages/experimental/plugin-market-bundle/README.md) is on, show the file for the active UI language as the plugin's detail document. Without `meta.detail` they show the package README, and an index entry page then falls back to the declared repository's README; the [document order](../../packages/util/plugin-detail/README.md#document-order) defines the rules. Write it for users, and reference images by paths relative to the file. Installed bundle cards and details, component lists and configuration details, and Settings' plugin inventory display this metadata, including disabled and preset plugins. Reads do not activate plugins. diff --git a/docs/cookbook/adding-a-package.zh.md b/docs/cookbook/adding-a-package.zh.md index 6ab95ce3b3..0d74f645dc 100644 --- a/docs/cookbook/adding-a-package.zh.md +++ b/docs/cookbook/adding-a-package.zh.md @@ -136,7 +136,7 @@ Append-only, prefix-stable, replacing, or independent behavior, including the ex } ``` -语言文件统一放在 `locale/`,并保留 `en.json` 作为发现入口。字段可选,填写时必须是非空字符串。英文 `meta.title` 和 `meta.description` 使用 sentence case:只大写首个单词、专有名词和产品名,例如 `Git worktrees`([rule](../../.agents/skills/dsh-prose-standard/SKILL.md#required-coverage-by-prose-location))。运行时文件或字段缺失时允许回退,JSON 损坏或字段无效时显示单插件诊断。仓库内的包和插件模板必须提供 `ja.json`,覆盖 `en.json` 或可选 `zh.json` 中的全部字段并保留占位符;详情路径可指向现有英文文档。`locale//` 下的子路径资源使用相同检查。英文描述面向产品用户说明可见行为,并以句末标点结束。运行 `pnpm run check:i18n` 检查 JSON 元数据和 UI 字典完整性;JSON 检查也接入 `check:all`、CI 静态检查和 hygiene。 +语言文件统一放在 `locale/`,并保留 `en.json` 作为发现入口。字段可选,填写时必须是非空字符串。英文 `meta.title` 和 `meta.description` 使用 sentence case:只大写首个单词、专有名词和产品名,例如 `Git worktrees`([rule](../../.agents/skills/dsh-prose-standard/SKILL.md#required-coverage-by-prose-location))。运行时文件或字段缺失时允许回退,JSON 损坏或字段无效时显示单插件诊断。仓库内的包和插件模板必须提供 `ja.json`、`es.json`、`fr.json`、`de.json`、`ru.json` 和 `pt.json`,覆盖 `en.json` 或可选 `zh.json` 中的全部字段并保留占位符、斜杠命令和 API 字段名;详情路径可指向现有英文文档。`locale//` 下的子路径资源使用相同检查。英文描述面向产品用户说明可见行为,并以句末标点结束。运行 `pnpm run check:i18n` 检查 JSON 元数据和 UI 字典完整性;JSON 检查也接入 `check:all`、CI 静态检查和 hygiene。 各字段先独立回退,再按页面规则格式化技术名称;回退优先经过现有 locale 语言链: @@ -147,7 +147,7 @@ Append-only, prefix-stable, replacing, or independent behavior, including the ex 要在组合包卡片、详情和组件行显示图片,包根插件在导出清单顶层设置 `"icon": "./icon.svg"`,或在省略该字段时导出 `./icon`。子路径插件导出自己的图标,例如 `"./search/icon": "./search.svg"`。将每张图片加入 `files`。清单 `icon` 相对于清单所在目录;绝对路径、URL、目录外路径,以及解析到目录外的符号链接均被拒绝。导出的图标像 locale 资源一样经 Node exports 解析,且必须位于所属包内。支持不超过 256 KiB 的 SVG、PNG、JPEG(`.jpg`/`.jpeg`)和 WebP 文件。图片必须自包含;SVG 作为图片渲染,不作为内联 HTML。Host 返回 data URL,不激活插件。声明无效或文件不可读时,显示元信息诊断并保留有效文本,不再尝试导出的图标;图片缺失或无法解码时使用面板的默认插画。 -组合包还可以把 `meta.detail` 设为相对于包根目录的 Markdown 文件路径,例如在 `locale/en.json` 中设为 `"docs/DETAIL.md"`,在 `locale/zh.json` 中设为 `"docs/DETAIL.zh.md"`;读取时使用包根目录下的这些文件。路径使用 `/`,必须位于包内,以 `.md` 或 `.markdown` 结尾,且没有包字段回退;图片放在该文件旁边,并按 `check-workspace-constraints` 的要求,在 `files` 中紧接语言资源之后列出该文件的顶层目录,例如 `docs`。该文件必须位于包的子目录中且已存在;包根目录下的文档会被拒绝,因为其旁边的图片不会被发布。组合包在**插件**中的页面,以及启用[自定义插件市场组合包](../../packages/experimental/plugin-market-bundle/README.zh.md)时其市场条目页面,把当前界面语言对应的该文件作为插件的详细介绍文档显示。没有 `meta.detail` 时,它们显示包 README;市场条目页面在此之后还会回退到所声明仓库的 README;规则由[文档顺序](../../packages/util/plugin-detail/README.zh.md#document-order)定义。该文档面向用户编写,图片使用相对于该文件的路径引用。 +组合包还可以把 `meta.detail` 设为相对于包根目录的 Markdown 文件路径,例如在 `locale/en.json` 中设为 `"docs/DETAIL.md"`,在 `locale/zh.json` 中设为 `"docs/DETAIL.zh.md"`;读取时使用包根目录下的这些文件。路径使用 `/`,必须位于包内,以 `.md` 或 `.markdown` 结尾,且没有包字段回退;图片放在该文件旁边,并按 `check-workspace-constraints` 的要求,在 `files` 中紧接语言资源之后列出该文件的顶层目录,例如 `docs`。该文件必须位于包的子目录中且已存在;包根目录下的文档会被拒绝,因为其旁边的图片不会被发布。组合包在**插件**中的页面,以及启用[自定义插件索引组合包](../../packages/experimental/plugin-market-bundle/README.zh.md)时其索引条目页面,把当前界面语言对应的该文件作为插件的详细介绍文档显示。没有 `meta.detail` 时,它们显示包 README;索引条目页面在此之后还会回退到所声明仓库的 README;规则由[文档顺序](../../packages/util/plugin-detail/README.zh.md#document-order)定义。该文档面向用户编写,图片使用相对于该文件的路径引用。 已安装 bundle 的卡片和详情、组件列表与配置详情、设置中的插件清单都展示这些元信息,包括禁用插件和预设内插件。读取时不激活插件。 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 32233ed894..acc3144c73 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -63,7 +63,7 @@ Application listeners are outside this package matrix. Desktop Host subscribes t | `plugin-manager/operation` | `parallel` | [`packages/boot/plugin-manager/src/types.ts:419`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`parallel`) | - | | `plugin-manager/update-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:447`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | | `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:168`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` | -| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` | +| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:484`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` | | `schedule/resolve-agent` | `waterfall` | [`packages/schedule/schedule/src/index.ts:74`](../packages/schedule/schedule/src/index.ts) | [`schedule`](../packages/schedule/schedule) (`waterfall`) | `server` | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:56`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) | @@ -108,7 +108,7 @@ Application listeners are outside this package matrix. Desktop Host subscribes t | `internal/service` | - | `gateway` | | `internal/status` | - | [`agent`](../packages/core/agent), `inspector`, `translator`, [`web`](../packages/web/web) | | `internal/update` | - | [`app-boot`](../packages/boot/app-boot) | -| `loader/volatile-update` | - | [`cua`](../packages/experimental/cua), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`plugin-market`](../packages/boot/plugin-market), `product-analytics`, `speech-to-text` | +| `loader/volatile-update` | - | [`cua`](../packages/experimental/cua), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`plugin-market`](../packages/boot/plugin-market), `product-analytics`, `speech-to-text` | | `slots/changed` | `ui-renderer` (`emit`) | - | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index ea2491b334..f199a70401 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -65,7 +65,7 @@ | `plugin-manager/operation` | `parallel` | [`packages/boot/plugin-manager/src/types.ts:419`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`parallel`) | - | | `plugin-manager/update-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:447`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | | `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:168`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` | -| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` | +| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:484`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` | | `schedule/resolve-agent` | `waterfall` | [`packages/schedule/schedule/src/index.ts:74`](../packages/schedule/schedule/src/index.ts) | [`schedule`](../packages/schedule/schedule) (`waterfall`) | `server` | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:56`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) | @@ -110,7 +110,7 @@ | `internal/service` | - | `gateway` | | `internal/status` | - | [`agent`](../packages/core/agent), `inspector`, `translator`, [`web`](../packages/web/web) | | `internal/update` | - | [`app-boot`](../packages/boot/app-boot) | -| `loader/volatile-update` | - | [`cua`](../packages/experimental/cua), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`plugin-market`](../packages/boot/plugin-market), `product-analytics`, `speech-to-text` | +| `loader/volatile-update` | - | [`cua`](../packages/experimental/cua), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`plugin-market`](../packages/boot/plugin-market), `product-analytics`, `speech-to-text` | | `slots/changed` | `ui-renderer` (`emit`) | - | diff --git a/docs/module-graph.md b/docs/module-graph.md index 727895bbda..7308d7df9d 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -730,7 +730,6 @@ flowchart TD pkg_llm_deepseek_account --> pkg_launch_environment pkg_llm_deepseek_account --> pkg_llm pkg_llm_deepseek_account --> pkg_llm_deepseek - pkg_llm_deepseek_account --> pkg_timeout pkg_tools --> pkg_agent pkg_tools --> pkg_llm pkg_tools --> pkg_scope @@ -1692,7 +1691,7 @@ flowchart TD | [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand) | | [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`llm-deepseek-account`](../packages/llm/llm-deepseek-account) | `llm` | [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`timeout`](../packages/util/timeout) | +| [`llm-deepseek-account`](../packages/llm/llm-deepseek-account) | `llm` | [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek) | | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) | | [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm) | | [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index b555876c4c..ecce1ba225 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -732,7 +732,6 @@ flowchart TD pkg_llm_deepseek_account --> pkg_launch_environment pkg_llm_deepseek_account --> pkg_llm pkg_llm_deepseek_account --> pkg_llm_deepseek - pkg_llm_deepseek_account --> pkg_timeout pkg_tools --> pkg_agent pkg_tools --> pkg_llm pkg_tools --> pkg_scope @@ -1694,7 +1693,7 @@ flowchart TD | [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand) | | [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`llm-deepseek-account`](../packages/llm/llm-deepseek-account) | `llm` | [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`timeout`](../packages/util/timeout) | +| [`llm-deepseek-account`](../packages/llm/llm-deepseek-account) | `llm` | [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek) | | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) | | [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm) | | [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index cc1521a2b8..c345f7e1b7 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -174,8 +174,8 @@ en: e5b55405bd2b69f5 zh: e5b55405bd2b69f5 /session-persistence-event-catalog/events/schedule/schedule-change-log-only: - en: 162584304e006c72 - zh: d8a6bacf60777aa9 + en: e73c202fb99159b6 + zh: b1f43bbd116f83da /session-persistence-event-catalog/events/session: en: a1ac8f2660a4f61e zh: a1ac8f2660a4f61e diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 67eb96e9ed..7edaa463de 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -813,7 +813,7 @@ Source: [`packages/sandbox/sandbox-policy/src/session-mode.ts:33`](../packages/s Types: [ScheduleChange](subsystems/schedule.md) -Source: [`packages/schedule/schedule/src/types.ts:365`](../packages/schedule/schedule/src/types.ts) +Source: [`packages/schedule/schedule/src/types.ts:371`](../packages/schedule/schedule/src/types.ts) ### `session/*` diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 11bc4861df..1d0d3e0bea 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -815,7 +815,7 @@ export type SessionEvent = { 类型:[ScheduleChange](subsystems/schedule.zh.md) -来源:[`packages/schedule/schedule/src/types.ts:365`](../packages/schedule/schedule/src/types.ts) +来源:[`packages/schedule/schedule/src/types.ts:371`](../packages/schedule/schedule/src/types.ts) ### `session/*` diff --git a/docs/subsystems/schedule.i18n.yaml b/docs/subsystems/schedule.i18n.yaml index a6e7893cf3..63354f0fd0 100644 --- a/docs/subsystems/schedule.i18n.yaml +++ b/docs/subsystems/schedule.i18n.yaml @@ -27,8 +27,8 @@ en: 93798ad69f74ddd5 zh: 634126ad8accc356 /host-wide-schedule/web-catalog: - en: b5e5ac8125e65f33 - zh: 9ee064d833a4f838 + en: 3377a013bf8cbfb7 + zh: c5666dc3f28b087f /host-wide-schedule/timing-edits: en: ce5d2095a3ad2e82 zh: 167b3715f8eaffc1 diff --git a/docs/subsystems/schedule.md b/docs/subsystems/schedule.md index ef24673f9b..13f1752a31 100644 --- a/docs/subsystems/schedule.md +++ b/docs/subsystems/schedule.md @@ -309,6 +309,14 @@ type ScheduleDeleteResult = | { readonly id: ScheduleId; readonly deleted: false; readonly code: 'schedule_not_found' } ``` +```ts type-equiv +/** Successful Host-wide removal of every ended task, whether or not any remained. */ +interface ScheduleDeleteInactiveResult { + /** Ended task rows this call removed; zero when the Host held none. */ + readonly deleted: number +} +``` + ## Web catalog The Remote `schedule.catalog()` method returns active and inactive Host tasks with their original Session bindings, ordered by `scheduledAt` ascending and then lexicographically by `id`. It reads only the Schedule domain, without activating Sessions or reading their history. Browser consumers receive this browser-safe type: @@ -337,7 +345,7 @@ type ScheduleCatalogEntry = ScheduleRecord & { } ``` -The Remote `schedule.list({ sessionId })`, model `schedule_list`, and Session-header catalog return only active tasks. A model view's derived timing `state` remains distinct from stored lifecycle `status`. Deletion uses `schedule.delete({ sessionId, id })` with the entry's original binding; a mismatched binding returns not found. Model tools supply the current Agent's Session, whereas the global user interface supplies the selected task's binding. The binding check alone does not establish caller authorization. The payload-free `schedule/changed` event invalidates client lists; reconnecting clients fetch current state again. +The Remote `schedule.list({ sessionId })`, model `schedule_list`, and Session-header catalog return only active tasks. A model view's derived timing `state` remains distinct from stored lifecycle `status`. Deletion uses `schedule.delete({ sessionId, id })` with the entry's original binding; a mismatched binding returns not found. Model tools supply the current Agent's Session, whereas the global user interface supplies the selected task's binding. The binding check alone does not establish caller authorization. `schedule.deleteInactive()` removes every ended (inactive) task across Sessions, with the saved delivery records each row carries; on success it answers `{ deleted }` with the count it removed; a normal failed batch rejects with nothing removed; an `unknown-state` failure may have removed rows and quarantines the Host schedule domain, so later schedule operations reject until it is reopened. Any rejection announces `schedule/changed`, and clients then re-read. The Automation tasks page's bulk cleanup uses it. An ended task never arms a timer, so the removal recomputes no target. The payload-free `schedule/changed` event invalidates client lists; reconnecting clients fetch current state again. The shipped Web composition mounts `ui-schedule` with the Host capability. The [client package](../../packages/client/ui-schedule/README.md) owns the catalog, empty state, and deletion controls. The page separately filters all, active, and inactive tasks, retains inactive details and an original-Session control in the detail tab strip, and requires explicit confirmed deletion. The Rules and Records tabs separate task settings from lazily paged saved receipts. Neither a receipt nor inactive status confirms model execution. @@ -508,6 +516,22 @@ async create(sessionId: SessionId, request: ScheduleCreateRequest, signal?: Abor */ @Remote('delete') async delete(request: ScheduleDeleteRequest, signal?: AbortSignal): Promise +/** + * Delete every ended Host task, leaving active tasks armed. + * + * Ended tasks never schedule another delivery, so the removal changes no pending + * target and no timer is recomputed. Rows are enumerated and removed in one FIFO + * slot, so a delivery ordered behind this call ends its task after the + * enumeration rather than during it. The batch is one domain write: a normal + * failure rejects with every listed row still stored, while a backend + * `unknown-state` failure may have removed rows and quarantines the Host + * domain, so later schedule operations reject until the domain is reopened. + * Every rejection announces `schedule/changed` because it does not establish + * which rows landed, and clients then re-read the catalog. + * @returns Count of removed rows; zero when the Host held no ended task. + */ +@Remote('deleteInactive') async deleteInactive(): Promise + /** * Update the name, instruction, and timing of an active task within the original Session * binding without activating the Session or changing saved deliveries. diff --git a/docs/subsystems/schedule.zh.md b/docs/subsystems/schedule.zh.md index 2d140c3821..320ae8e743 100644 --- a/docs/subsystems/schedule.zh.md +++ b/docs/subsystems/schedule.zh.md @@ -309,6 +309,14 @@ type ScheduleDeleteResult = | { readonly id: ScheduleId; readonly deleted: false; readonly code: 'schedule_not_found' } ``` +```ts type-equiv +/** Successful Host-wide removal of every ended task, whether or not any remained. */ +interface ScheduleDeleteInactiveResult { + /** Ended task rows this call removed; zero when the Host held none. */ + readonly deleted: number +} +``` + ## Web 目录 Remote 方法 `schedule.catalog()` 返回活动和未运行的宿主任务及其原始 Session 绑定,先按 `scheduledAt` 升序排列,再按 `id` 字典序排列。它只读取 Schedule domain,不激活 Session 或读取其历史。浏览器消费方接收以下可安全用于浏览器的类型: @@ -337,7 +345,7 @@ type ScheduleCatalogEntry = ScheduleRecord & { } ``` -Remote 方法 `schedule.list({ sessionId })`、模型 `schedule_list` 和 Session 页头目录仅返回活动任务。模型视图派生的时间 `state` 与存储的生命周期 `status` 仍是不同概念。删除通过 `schedule.delete({ sessionId, id })` 使用条目的原始绑定;绑定不匹配时返回未找到。模型工具传入当前 Agent 的 Session,全局用户界面则传入所选任务的绑定。仅校验绑定并不构成调用者鉴权。无 payload 的 `schedule/changed` 事件通知客户端刷新列表;重新连接后,客户端再次获取当前状态。 +Remote 方法 `schedule.list({ sessionId })`、模型 `schedule_list` 和 Session 页头目录仅返回活动任务。模型视图派生的时间 `state` 与存储的生命周期 `status` 仍是不同概念。删除通过 `schedule.delete({ sessionId, id })` 使用条目的原始绑定;绑定不匹配时返回未找到。模型工具传入当前 Agent 的 Session,全局用户界面则传入所选任务的绑定。仅校验绑定并不构成调用者鉴权。`schedule.deleteInactive()` 删除跨会话的全部未运行(已结束)任务,连同每行携带的已保存的发送记录;成功时以它移除的计数答复 `{ deleted }`;普通失败时不删除任何行;后端报告 `unknown-state` 时可能已移除部分行,并会隔离 Host 的 schedule 领域,之后该领域的操作都会拒绝,直到重新打开。任何拒绝都会先发出 `schedule/changed`,客户端重新读取。自动化任务页面的批量清理使用该方法。未运行任务从不会安排定时器,因此该删除不重算任何目标。无 payload 的 `schedule/changed` 事件通知客户端刷新列表;重新连接后,客户端再次获取当前状态。 随发行版交付的 Web 组合把 `ui-schedule` 与宿主能力一起挂载。[客户端包](../../packages/client/ui-schedule/README.zh.md) 负责目录、空状态与删除控件。页面单独筛选全部、活动和未运行任务,保留未运行任务详情及详情页签条内的原 Session 入口,并要求显式确认删除。“规则”和“任务运行记录”页签将任务设置与按需分页加载的已保存回执分开。回执与未运行状态均不确认模型执行。 @@ -508,6 +516,22 @@ async create(sessionId: SessionId, request: ScheduleCreateRequest, signal?: Abor */ @Remote('delete') async delete(request: ScheduleDeleteRequest, signal?: AbortSignal): Promise +/** + * Delete every ended Host task, leaving active tasks armed. + * + * Ended tasks never schedule another delivery, so the removal changes no pending + * target and no timer is recomputed. Rows are enumerated and removed in one FIFO + * slot, so a delivery ordered behind this call ends its task after the + * enumeration rather than during it. The batch is one domain write: a normal + * failure rejects with every listed row still stored, while a backend + * `unknown-state` failure may have removed rows and quarantines the Host + * domain, so later schedule operations reject until the domain is reopened. + * Every rejection announces `schedule/changed` because it does not establish + * which rows landed, and clients then re-read the catalog. + * @returns Count of removed rows; zero when the Host held no ended task. + */ +@Remote('deleteInactive') async deleteInactive(): Promise + /** * Update the name, instruction, and timing of an active task within the original Session * binding without activating the Session or changing saved deliveries. diff --git a/docs/subsystems/slots.i18n.yaml b/docs/subsystems/slots.i18n.yaml index cf71b1d1af..17ae54903e 100644 --- a/docs/subsystems/slots.i18n.yaml +++ b/docs/subsystems/slots.i18n.yaml @@ -3,8 +3,8 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/slots.md /web-client-slots: - en: dd504c406c346f45 - zh: 5dadae8b00aafc5b + en: 808a284f5a4280b5 + zh: 1650d6c3a94eae6e /web-client-slots/declaration-and-lifecycle: en: f2d2c0d3b8f14ec7 zh: 441ed13ff3f81d2a diff --git a/docs/subsystems/slots.md b/docs/subsystems/slots.md index 998aebbffc..ee31122d13 100644 --- a/docs/subsystems/slots.md +++ b/docs/subsystems/slots.md @@ -10,7 +10,7 @@ This page documents slot ownership, component inputs, extension APIs, and the sh The plugin catalog declares `plugins.add.actions`, a root-scoped list for `MenuItemButton` rows after installation in the Add plugin menu. Its owner supplies `onDismiss()` to close the menu before starting an action. The slot follows the owning Plugins page declaration lifetime. -The root-scoped lists `sidebar.panellist.actions`, `conversation.session.header.views.actions`, and `sidebar.right.tab.guide.actions` supply the rows of the More menus after the panel rows, the View tabs, and the right-sidebar guide cards. A region draws its More button and menu only while its list has registrations, and passes the customizable entries with their pin state, `onOpen`, `onPinnedChange`, and `onDismiss`. Each region persists its own unpinned entry ids; [`ui-entry-customization`](../../packages/client/ui-entry-customization/README.md) registers the shipped rows. +The root-scoped lists `sidebar.panellist.actions`, `conversation.session.header.views.actions`, and `sidebar.right.tab.guide.actions` supply the rows of the More menus after the panel rows, the View tabs, and the right-sidebar guide cards. A region draws its More button and menu only while its list has registrations, and passes the customizable entries with their pin state, `onOpen`, `onPinnedChange`, and `onDismiss`. Panel and View entries also carry optional `active` state for the current selection, independently of pin state. Each region keeps its own unpinned entry ids in memory; [`ui-entry-customization`](../../packages/client/ui-entry-customization/README.md) registers the shipped rows. [ui-workspace](../../packages/client/ui-workspace/README.md#understand-the-implementation) documents Session-row extension positions, visibility, and keyboard behavior. diff --git a/docs/subsystems/slots.zh.md b/docs/subsystems/slots.zh.md index e2d9217ec6..ca13d60008 100644 --- a/docs/subsystems/slots.zh.md +++ b/docs/subsystems/slots.zh.md @@ -10,7 +10,7 @@ Slots 是 Web Client 的类型化 React 组合系统。[`dsh-client-ui-slots`](. 插件目录声明 `plugins.add.actions`,它是 root 作用域的 list,在添加插件菜单的安装项之后提供 `MenuItemButton` 行。owner 提供 `onDismiss()`,用于开始动作前关闭菜单。该 slot 遵循所属插件页面声明的生命周期。 -root 作用域的 list `sidebar.panellist.actions`、`conversation.session.header.views.actions` 和 `sidebar.right.tab.guide.actions` 提供面板行、View 标签和右侧栏引导卡片之后“更多”菜单的菜单行。区域只在其 list 有注册项时画“更多”按钮与菜单,并传入可自定义的入口及其 Pin 状态、`onOpen`、`onPinnedChange` 与 `onDismiss`。各区域自行持久化已收起的入口 id;[`ui-entry-customization`](../../packages/client/ui-entry-customization/README.zh.md) 注册随产品提供的菜单行。 +root 作用域的 list `sidebar.panellist.actions`、`conversation.session.header.views.actions` 和 `sidebar.right.tab.guide.actions` 提供面板行、View 标签和右侧栏引导卡片之后“更多”菜单的菜单行。区域只在其 list 有注册项时画“更多”按钮与菜单,并传入可自定义的入口及其 Pin 状态、`onOpen`、`onPinnedChange` 与 `onDismiss`。面板和 View 入口还通过可选的 `active` 状态标识当前选中项,该状态独立于 Pin 状态。各区域在内存中维护已收起的入口 id;[`ui-entry-customization`](../../packages/client/ui-entry-customization/README.zh.md) 注册随产品提供的菜单行。 [ui-workspace](../../packages/client/ui-workspace/README.zh.md#understand-the-implementation) 记录 Session 行的扩展位置、显示条件和键盘行为。 diff --git a/docs/subsystems/storage.i18n.yaml b/docs/subsystems/storage.i18n.yaml index 632e0788df..5f4126783c 100644 --- a/docs/subsystems/storage.i18n.yaml +++ b/docs/subsystems/storage.i18n.yaml @@ -9,14 +9,14 @@ en: b11e06e7c0e0fd13 zh: 4585f26ff416cb41 /storage/the-backend-contract: - en: 2316787902c6a94e - zh: e3bda06e1c33acac + en: 225e25b9b4b08865 + zh: 53bd3554da610607 /storage/declaring-a-domain: en: 775cce941500f01f zh: 78acbc67f48574cb /storage/the-open-domain: - en: 2468b5df66c49b51 - zh: 1587f460e9769db2 + en: b69b25bc06f1c052 + zh: 8aca58718da520c7 /storage/the-domain-facility-ctx-storagedomain: en: deed74fcd4506ef4 zh: 8e3e6e5d2dc2e989 diff --git a/docs/subsystems/storage.md b/docs/subsystems/storage.md index 674221a263..8dddb5818e 100644 --- a/docs/subsystems/storage.md +++ b/docs/subsystems/storage.md @@ -44,7 +44,7 @@ interface StorageBackend { } ``` -A backend owns one medium (a file-tree root, a database file) and exposes optional operation groups; `kv` is the only shipped group. `KvFacet.open(descriptor)` opens one named unit — `KvUnitDescriptor` carries the name, current format version, optional compatible record versions, table names, and whether a global singleton slot exists — and returns a `KvUnit` with `loadAll`, `putRecord`, `deleteRecord`, `setGlobal`, and `close`. Unit and table names must match `UNIT_NAME_RE` (safe as a file name and as a SQL identifier segment); record keys are arbitrary strings that never reach file paths. A unit does not serialize concurrent writes — ordering belongs to the caller — but each single call is atomic on the medium and durable once resolved. A `single` medium stamped with a different version rejects `version-mismatch`; a `per-record` document stamped outside the accepted set reads as absent. A medium that cannot be parsed as the unit rejects `malformed-medium`. [`backend.ts`](../../packages/storage/storage/src/backend.ts) is the normative clause-by-clause contract, and the shared conformance suite in [`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) checks every clause against each backend. The [json backend](../../packages/storage/storage-json/README.md) republishes one whole human-readable file per unit atomically; the [sqlite backend](../../packages/storage/storage-sqlite/README.md) stores one document per row in one database for frequently updated data. +A backend owns one medium (a file-tree root, a database file) and exposes optional operation groups; `kv` is the only shipped group. `KvFacet.open(descriptor)` opens one named unit — `KvUnitDescriptor` carries the name, current format version, optional compatible record versions, table names, and whether a global singleton slot exists — and returns a `KvUnit` with `loadAll`, `putRecord`, `deleteRecord`, `deleteRecords`, `setGlobal`, and `close`. Unit and table names must match `UNIT_NAME_RE` (safe as a file name and as a SQL identifier segment); record keys are opaque strings in the `single` layout and match `[a-zA-Z0-9_-]+` path segments in the `per-record` layout. A unit does not serialize concurrent writes — ordering belongs to the caller — but each single call is atomic on the medium; durability holds for single-record writes and whole-file publishes, while a `per-record` batch deletion relies on the filesystem persisting directory-entry updates. `deleteRecords` applies a whole batch in one call: it resolves once every listed record is absent from the readable state and the backend accepted the batch, and a rejection normally leaves the batch unapplied; a backend that cannot establish or restore the batch's state rejects with `unknown-state`, after which every later operation on that unit rejects with the same error until `close()` releases it (calls after close reject `closed`). A `single`-layout failure after the atomic replacement (a directory-fsync error) can leave the new file on the medium while the caller sees a rejection; the unit keeps its memory consistent and the next publish rewrites the file. A `single` medium stamped with a different version rejects `version-mismatch`; a `per-record` document stamped outside the accepted set reads as absent. A medium that cannot be parsed as the unit rejects `malformed-medium`. [`backend.ts`](../../packages/storage/storage/src/backend.ts) is the normative clause-by-clause contract, and the shared conformance suite in [`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) checks the shared clauses against each backend. The [json backend](../../packages/storage/storage-json/README.md) republishes one whole human-readable file per unit atomically; the [sqlite backend](../../packages/storage/storage-sqlite/README.md) stores one document per row in one database for frequently updated data. ## Declaring a domain @@ -123,7 +123,7 @@ interface Domain { } ``` -Reads are synchronous from authoritative in-memory state: `KvTable` exposes `get`/`entries`/`keys`/`size` (snapshot iterators that stay stable while queued writes land), and the global handle's `get()` serves the spec's `initial` until the first `set` materializes the slot on the medium. Every write — `put`, `delete`, `update`, `global.set` — queues on one per-domain chain and reaches backend durability first, then mutates memory, then emits `domain/changed`; a rejected backend write leaves memory untouched, so reads never diverge from the medium. `update(key, fn)` is an atomic read-modify-write at its chain slot (a missing key rejects `missing-key`); `delete` of an absent key resolves `false` with no write and no event. Returned records are the stored objects themselves, not copies — replace via `put`/`update`, never mutate in place. +Reads are synchronous from authoritative in-memory state: `KvTable` exposes `get`/`entries`/`keys`/`size` (snapshot iterators that stay stable while queued writes land), and the global handle's `get()` serves the spec's `initial` until the first `set` materializes the slot on the medium. Every write — `put`, `delete`, `deleteMany`, `update`, `global.set` — queues on one per-domain chain and reaches backend durability first, then mutates memory, then emits `domain/changed`; a rejected backend write leaves memory untouched, so reads never diverge from the medium; a backend that reports `unknown-state` instead quarantines the domain, and its reads and later writes reject until `close()` releases it. `update(key, fn)` is an atomic read-modify-write at its chain slot (a missing key rejects `missing-key`); `delete` of an absent key resolves `false` with no write and no event. `deleteMany(keys)` removes the present keys as one backend batch in one chain slot, resolves the removed count, and emits one `domain/changed` per removed key; a rejected backend write leaves every record in place and emits nothing; an `unknown-state` rejection quarantines the domain. Returned records are the stored objects themselves, not copies — replace via `put`/`update`, never mutate in place. ## The domain facility: `ctx.storageDomain` diff --git a/docs/subsystems/storage.zh.md b/docs/subsystems/storage.zh.md index 2a4366c84e..7411fa632b 100644 --- a/docs/subsystems/storage.zh.md +++ b/docs/subsystems/storage.zh.md @@ -44,7 +44,7 @@ interface StorageBackend { } ``` -一个后端拥有一个介质(一棵文件树的根目录、一个数据库文件),并提供可选的操作组;`kv` 是唯一已交付的操作组。`KvFacet.open(descriptor)` 打开一个具名 unit——`KvUnitDescriptor` 携带名称、当前格式版本、可选的兼容记录版本、表名清单,以及是否存在全局单例 slot——并返回提供 `loadAll`、`putRecord`、`deleteRecord`、`setGlobal` 和 `close` 的 `KvUnit`。unit 名与表名必须匹配 `UNIT_NAME_RE`(既可安全用作文件名,也可安全用作 SQL 标识符片段);记录键是任意字符串,绝不进入文件路径。unit 不对并发写入做串行化——顺序由调用方负责——但每次单独调用在介质上都是原子的,且 resolve 后即已持久。`single` 介质上记录的版本不同时拒绝 `version-mismatch`;`per-record` 文档的版本在接受集合之外时读作不存在。无法按该 unit 解析的介质拒绝 `malformed-medium`。[`backend.ts`](../../packages/storage/storage/src/backend.ts) 是逐条款的规范性约定,[`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) 中的共享一致性套件会针对每个后端检查每项条款。[json 后端](../../packages/storage/storage-json/README.zh.md)以原子方式为每个 unit 整文件重新发布一份人类可读文件;[sqlite 后端](../../packages/storage/storage-sqlite/README.zh.md)在单个数据库中每行存储一份文档,用于频繁更新的数据。 +一个后端拥有一个介质(一棵文件树的根目录、一个数据库文件),并提供可选的操作组;`kv` 是唯一已交付的操作组。`KvFacet.open(descriptor)` 打开一个具名 unit——`KvUnitDescriptor` 携带名称、当前格式版本、可选的兼容记录版本、表名清单,以及是否存在全局单例 slot——并返回提供 `loadAll`、`putRecord`、`deleteRecord`、`deleteRecords`、`setGlobal` 和 `close` 的 `KvUnit`。unit 名与表名必须匹配 `UNIT_NAME_RE`(既可安全用作文件名,也可安全用作 SQL 标识符片段);记录键在 `single` 布局中是不透明字符串,在 `per-record` 布局中则是匹配 `[a-zA-Z0-9_-]+` 的路径片段。unit 不对并发写入做串行化——顺序由调用方负责——但每次单独调用在介质上都是原子的;单记录写入与整文件发布在 resolve 后即已持久,`per-record` 批量删除则依赖文件系统对目录项更新的持久化。`deleteRecords` 以一次调用应用整批删除:所有列出的记录都从可读状态消失且后端接受该批后才 resolve,拒绝时通常整批未应用;无法建立或恢复批次状态的批量删除会以 `unknown-state` 拒绝,之后该 unit 的所有操作都以同一错误拒绝,直到 `close()` 释放它(关闭后的调用以 `closed` 拒绝)。`single` 布局在原子替换之后失败(目录 fsync 错误)时,新文件可能已落在介质上而调用方看到拒绝;unit 的内存保持一致,下一次发布会重写该文件。`single` 介质上记录的版本不同时拒绝 `version-mismatch`;`per-record` 文档的版本在接受集合之外时读作不存在。无法按该 unit 解析的介质拒绝 `malformed-medium`。[`backend.ts`](../../packages/storage/storage/src/backend.ts) 是逐条款的规范性约定,[`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) 中的共享一致性套件会针对每个后端检查共享条款。[json 后端](../../packages/storage/storage-json/README.zh.md)以原子方式为每个 unit 整文件重新发布一份人类可读文件;[sqlite 后端](../../packages/storage/storage-sqlite/README.zh.md)在单个数据库中每行存储一份文档,用于频繁更新的数据。 ## 声明领域 @@ -123,7 +123,7 @@ interface Domain { } ``` -读取是同步的,来自权威的内存态:`KvTable` 暴露 `get`/`entries`/`keys`/`size`(快照迭代器,在排队写入落地期间保持稳定),global 句柄的 `get()` 在第一次 `set` 将 slot 物化到介质之前一直返回 spec 的 `initial`。每次写入——`put`、`delete`、`update`、`global.set`——都在同一条逐领域写链上排队,先在后端完成持久化,再更新内存,最后发出 `domain/changed`;后端写入被拒时内存原样不动,因此读取绝不会偏离介质。`update(key, fn)` 在其写链 slot 上是一次原子的读-改-写(键缺失时拒绝 `missing-key`);`delete` 一个不存在的键 resolve 为 `false`,不产生写入也不产生事件。返回的记录就是存储的对象本身,不是副本——请经 `put`/`update` 整体替换,绝不要就地修改。 +读取是同步的,来自权威的内存态:`KvTable` 暴露 `get`/`entries`/`keys`/`size`(快照迭代器,在排队写入落地期间保持稳定),global 句柄的 `get()` 在第一次 `set` 将 slot 物化到介质之前一直返回 spec 的 `initial`。每次写入——`put`、`delete`、`deleteMany`、`update`、`global.set`——都在同一条逐领域写链上排队,先在后端完成持久化,再更新内存,最后发出 `domain/changed`;后端写入被拒时内存原样不动,因此读取绝不会偏离介质;若后端报告 `unknown-state`,领域会被隔离,读取与后续写入都会拒绝,直到 `close()` 释放它。`update(key, fn)` 在其写链 slot 上是一次原子的读-改-写(键缺失时拒绝 `missing-key`);`delete` 一个不存在的键 resolve 为 `false`,不产生写入也不产生事件。`deleteMany(keys)` 在一个写链 slot 内把存在的键作为一次后端批量删除移除,resolve 为移除数量,并为每个被移除的键发一次 `domain/changed`;后端写入被拒时所有记录保持原样,也不发事件;`unknown-state` 拒绝会隔离整个领域。返回的记录就是存储的对象本身,不是副本——请经 `put`/`update` 整体替换,绝不要就地修改。 ## 领域 facility:`ctx.storageDomain` diff --git a/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.i18n.yaml index a13ae70849..9e6e37a1bf 100644 --- a/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.i18n.yaml @@ -3,14 +3,14 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.md /: - en: 383326881143f69b - zh: cd7ff0e2b97f9c2f -/japanese-is-a-built-in-language: - en: f97bae63a42a3627 - zh: cec418f8bf6ac168 -/japanese-is-a-built-in-language/change: - en: feb1f2d1cafdcfe0 - zh: 94d527865ec108bb -/japanese-is-a-built-in-language/migration: - en: ced2c2baad9124b4 - zh: 7b0b5b0e345994b8 + en: cfd1ffd2cd268220 + zh: 2ed03b1063f3319d +/built-in-languages-replace-duplicate-language-packs: + en: 5855b69ac12a5f50 + zh: a81de3239837f1c4 +/built-in-languages-replace-duplicate-language-packs/change: + en: 665e68d784661f15 + zh: 2fff6fcfceea7994 +/built-in-languages-replace-duplicate-language-packs/migration: + en: 6f77e0b507e09ca7 + zh: 59e0c71780f06e9e diff --git a/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.md b/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.md index 256e77ae05..5c6cf0eaea 100644 --- a/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.md +++ b/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.md @@ -1,18 +1,18 @@ --- kind: upgrade-guide -description: "Built-in Japanese registrations conflict with third-party Japanese language packs." +description: "Built-in language registrations conflict with third-party language packs using the same ids." --- -# Japanese is a built-in language +# Built-in languages replace duplicate language packs English | [中文](guide.zh.md) ## Change -The locale plugin registers the `ja` language and Japanese dictionaries for `common` and `settings.locale`. Third-party language packs following the previous Japanese README example now fail with an already-registered language or dictionary error. +The locale plugin registers `ja`, `es`, `fr`, `de`, `ru`, and `pt`, with dictionaries for `common`, `settings.locale`, and the built-in UI plugins. Third-party language packs using these ids now fail with an already-registered language or dictionary error, including packs based on the earlier Japanese or French README examples. ## Migration -1. In the language pack's client entry, remove `ctx.locale.addLanguage({ id: 'ja', ... })` and registrations for `common` and `settings.locale` under `ja`. Remove other Japanese dictionary registrations whose namespaces are now supplied by built-in plugins; keep translations for namespaces owned by your own plugins. -2. If the pack represents a distinct language variant, register a separate BCP 47-style id, such as `ja-JP`, for both the definition and its dictionaries instead of `ja`. +1. In the language pack's client entry, remove `ctx.locale.addLanguage(...)` for these ids and dictionary registrations in namespaces supplied by built-in plugins. Keep translations for namespaces owned by your own plugins. +2. If the pack represents a distinct language variant, register a separate BCP 47-style id, such as `fr-CA`, for both the definition and its dictionaries instead of the built-in id. 3. Reload the application with the pack enabled. Confirm it starts without duplicate-registration errors and that selecting the intended language displays its translations. diff --git a/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.zh.md index 80382ebe9e..dd98916cc6 100644 --- a/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-rc.2/built-in-japanese/guide.zh.md @@ -1,18 +1,18 @@ --- kind: upgrade-guide -description: "内置日语注册会与第三方日语语言包冲突。" +description: "内置语言注册会与使用相同 id 的第三方语言包冲突。" --- -# 日语成为内置语言 +# 内置语言取代重复的语言包 [English](guide.md) | 中文 ## 变更 -locale 插件注册 `ja` 语言,以及 `common` 和 `settings.locale` 的日文字典。按此前 README 日语示例编写的第三方语言包会因语言或字典已注册而加载失败。 +locale 插件注册 `ja`、`es`、`fr`、`de`、`ru` 和 `pt`,并提供 `common`、`settings.locale` 以及内置 UI 插件的字典。使用这些 id 的第三方语言包会因语言或字典已注册而加载失败,包括按此前 README 日语或法语示例编写的语言包。 ## 迁移 -1. 在语言包的 client 入口中,删除 `ctx.locale.addLanguage({ id: 'ja', ... })`,以及 `common` 和 `settings.locale` 下的 `ja` 注册。删除其他已由内置插件提供的命名空间的日语字典注册;保留自己插件所拥有命名空间的翻译。 -2. 如果语言包代表不同的语言变体,为语言定义及其字典注册独立的 BCP 47 风格 id,例如 `ja-JP`,代替 `ja`。 +1. 在语言包的 client 入口中,删除这些 id 的 `ctx.locale.addLanguage(...)`,以及内置插件已提供的命名空间下的字典注册。保留自己插件所拥有命名空间的翻译。 +2. 如果语言包代表不同的语言变体,为语言定义及其字典注册独立的 BCP 47 风格 id,例如 `fr-CA`,代替内置 id。 3. 启用语言包并重新加载应用。确认启动时没有重复注册错误,且选择目标语言后显示对应译文。 diff --git a/docs/user/guide/plugin-market.i18n.yaml b/docs/user/guide/plugin-market.i18n.yaml index 3a940f4be4..28f727d0e0 100644 --- a/docs/user/guide/plugin-market.i18n.yaml +++ b/docs/user/guide/plugin-market.i18n.yaml @@ -2,33 +2,33 @@ # section, a hash of its English and Chinese blocks outside code blocks and generated regions. # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/user/guide/plugin-market.md -/plugin-markets: - en: 09b2a6328dcae63c - zh: ac7bce767e4f5011 -/plugin-markets/summary: - en: 2716b194b8a12aa7 - zh: b90773d095851969 -/plugin-markets/table-of-contents: +/plugin-indexes: + en: 5ee6a364e2708ab9 + zh: 369781529be73628 +/plugin-indexes/summary: + en: a1d8f17e7c4b8357 + zh: 13b95dcedde344f2 +/plugin-indexes/table-of-contents: en: a8d1e6785c3c3c30 zh: 7ea1eaa8204a5a1e -/plugin-markets/turn-on-discover: - en: a5024e57052f6e36 - zh: 39b84e247adc9a18 -/plugin-markets/browse-and-install: - en: cf616bba26958533 - zh: 1e32df16a6f4bba7 -/plugin-markets/publish-an-index: +/plugin-indexes/turn-on-discover: + en: f98bc0913d4bd046 + zh: 104094714786507e +/plugin-indexes/browse-and-install: + en: 0f5a4082d5debe9b + zh: c0e53f186cb31cd4 +/plugin-indexes/publish-an-index: en: b85b82b22c1e14b8 zh: 920c0d6f20956c07 -/plugin-markets/generate-an-index-with-the-reference-generator: +/plugin-indexes/generate-an-index-with-the-reference-generator: en: 6501406f530ffe33 zh: 700acae3f466c985 -/plugin-markets/index-format-reference: - en: d058d12c27433f64 - zh: f43b85e4934817f3 -/plugin-markets/further-exploration: - en: 3c0a90e8bf73c89b - zh: 95608eb4f6278cb4 -/plugin-markets/dev-note: +/plugin-indexes/index-format-reference: + en: 8fed66180a76f815 + zh: 7069ac4eed4bdfd9 +/plugin-indexes/further-exploration: + en: 2223e92806e7e79a + zh: 093309e1c40caa6c +/plugin-indexes/dev-note: en: b75c8458928b72b9 zh: 003ba4e17b4b5269 diff --git a/docs/user/guide/plugin-market.md b/docs/user/guide/plugin-market.md index a690e46c55..45e2629324 100644 --- a/docs/user/guide/plugin-market.md +++ b/docs/user/guide/plugin-market.md @@ -1,10 +1,10 @@ -# Plugin markets +# Plugin indexes English | [中文](plugin-market.zh.md) ## Summary -A plugin market is one JSON index file that lists installable DSH bundles with their titles, icons, authors, and versions. Turn on the experimental **Custom plugin markets** bundle to browse markets in a **Discover** tab on the Plugins page and install a listed version after a confirmation. To publish a market, write or generate an index file and serve it, together with its icon files, from any static host. +A plugin index is one JSON file that lists installable DSH bundles with their titles, icons, authors, and versions. Turn on the experimental **Custom plugin indexes** bundle to browse indexes in a **Discover** tab on the Plugins page and install a listed version after a confirmation. To publish an index, write or generate an index file and serve it, together with its icon files, from any static host. ## Table of Contents @@ -18,20 +18,20 @@ A plugin market is one JSON index file that lists installable DSH bundles with t ## Turn on Discover -Open **Plugins**, find **Custom plugin markets** under **Experimental plugins**, and switch it on. A **Discover** tab appears next to **Installed**, and Creator mode gains the read-only `plugin_market` tool. Switching the bundle off removes both again. +Open **Plugins**, find **Custom plugin indexes** under **Experimental plugins**, and switch it on. A **Discover** tab appears next to **Installed**, and Creator mode gains the read-only `plugin_market` tool. Switching the bundle off removes both again. -DSH ships no index source. Select **Discover**, choose **Add index source**, and enter the URL of an index file. A URL must use `http:` or `https:` and must not contain a user name, password, or `#` fragment. Anyone on the network path can change an `http:` index, including the install specs it lists, so use `https:` for a market reached over a network you do not control. Each source belongs to the current profile. +DSH ships no index source. Select **Discover**, choose **Add index source**, and enter the URL of an index file. A URL must use `http:` or `https:` and must not contain a user name, password, or `#` fragment. Anyone on the network path can change an `http:` index, including the install specs it lists, so use `https:` for an index reached over a network you do not control. Each source belongs to the current profile. ## Browse and install -The market menu at the left of the toolbar switches between sources and opens **Manage index sources…**, where each source shows its last error. Search matches names, authors, keywords, and titles, descriptions, categories, and labels in every language, and tolerates typos; results come in best-match order until you choose another. The **Labels** menu, category pills, and **Sort** menu use the categories, labels, and sort keys that the market declares, and **Publish date** sorts by each plugin's latest version. +The index menu at the left of the toolbar switches between sources and opens **Manage index sources…**, where each source shows its last error. Search matches names, authors, keywords, and titles, descriptions, categories, and labels in every language, and tolerates typos; results come in best-match order until you choose another. The **Labels** menu, category pills, and **Sort** menu use the categories, labels, and sort keys that the index declares, and **Publish date** sorts by each plugin's latest version. Each card shows the plugin's recommended version: the highest version compatible with the running DSH that is not a prerelease, or the highest compatible prerelease when the plugin lists no version without a prerelease tag. **Install** opens a confirmation with that version's source: the npm package and registry, or the Git repository, commit, and directory. Confirming starts the normal plugin installation, including its compatibility and registry checks; nothing is installed without that confirmation. Select a card to open its page. The **Versions** list shows every listed version with its compatibility and offers **Install** for any version DSH accepts, including older releases and prereleases. A card that shows **Installed**, **Update available**, or **Provided by DSH** opens the installed bundle instead. -Markets are third-party content. A market chooses each plugin's title, description, icon, author, and source; DSH checks the file format but not those claims. Add only sources you trust, and check the source in the confirmation before installing. +Indexes are third-party content. An index chooses each plugin's title, description, icon, author, and source; DSH checks the file format but not those claims. Add only sources you trust, and check the source in the confirmation before installing. ## Publish an index @@ -109,10 +109,10 @@ Text fields marked `LocalizedText` are a string or an object of strings keyed by | Index field | Required | Meaning | |---|---|---| | `format` | yes | `1`. | -| `title`, `description` | `title` only | Market name and description, `LocalizedText`. | +| `title`, `description` | `title` only | Index name and description, `LocalizedText`. | | `metadata.categories`, `metadata.labels` | no | Objects mapping an id to `{ "title": LocalizedText }`. | | `metadata.sortKeys` | no | Object mapping an id to `{ "title": LocalizedText, "order": "asc" \| "desc" }`; `order` is the initial direction. | -| `plugins` | yes | Plugins in the market's default order. | +| `plugins` | yes | Plugins in the index's default order. | | Plugin field | Required | Meaning | |---|---|---| @@ -140,7 +140,7 @@ DSH installs an npm version as `name@version`, asking `registry` first when pres ## Further Exploration - [Discover tab reference](../../../packages/client/ui-plugin-market/README.md) describes the tab's controls and states. -- [Plugin market service](../../../packages/boot/plugin-market/README.md) covers fetch limits, caching, and the `plugin_market` tool. +- [Plugin index service](../../../packages/boot/plugin-market/README.md) covers fetch limits, caching, and the `plugin_market` tool. - [Index validator](../../../packages/util/plugin-market-index/README.md) is the library generators can use to check an index before publishing it. ## Dev Note diff --git a/docs/user/guide/plugin-market.zh.md b/docs/user/guide/plugin-market.zh.md index 2385d0a75b..423b463a57 100644 --- a/docs/user/guide/plugin-market.zh.md +++ b/docs/user/guide/plugin-market.zh.md @@ -1,10 +1,10 @@ -# 插件市场 +# 插件索引 [English](plugin-market.md) | 中文 ## 概述 -插件市场是一个 JSON 索引文件,列出可安装的 DSH 组合包(bundle)及其标题、图标、作者和版本。打开实验性的**自定义插件市场**组合包后,可以在插件页的**发现**标签页中浏览市场,并在确认后安装列出的版本。要发布市场,编写或生成一个索引文件,并把它和图标文件一起放到任意静态主机上。 +插件索引是一个 JSON 文件,列出可安装的 DSH 组合包(bundle)及其标题、图标、作者和版本。打开实验性的**自定义插件索引**组合包后,可以在插件页的**发现**标签页中浏览索引,并在确认后安装列出的版本。要发布索引,编写或生成一个索引文件,并把它和图标文件一起放到任意静态主机上。 ## 目录 @@ -18,20 +18,20 @@ ## 打开「发现」 -打开**插件**,在**实验性插件**下找到**自定义插件市场**并打开开关。**已安装**旁边会出现**发现**标签页,Creator 模式也会获得只读的 `plugin_market` 工具。关闭该组合包会再次移除这两者。 +打开**插件**,在**实验性插件**下找到**自定义插件索引**并打开开关。**已安装**旁边会出现**发现**标签页,Creator 模式也会获得只读的 `plugin_market` 工具。关闭该组合包会再次移除这两者。 -DSH 不内置任何索引源。选择**发现**,点击**添加索引源**,输入一个索引文件的 URL。URL 必须使用 `http:` 或 `https:`,并且不能包含用户名、密码或 `#` 片段。网络路径上的任何人都能修改 `http:` 索引,包括其中列出的安装 spec,因此经由不受你控制的网络访问的市场应使用 `https:`。每个索引源属于当前 profile。 +DSH 不内置任何索引源。选择**发现**,点击**添加索引源**,输入一个索引文件的 URL。URL 必须使用 `http:` 或 `https:`,并且不能包含用户名、密码或 `#` 片段。网络路径上的任何人都能修改 `http:` 索引,包括其中列出的安装 spec,因此经由不受你控制的网络访问的索引应使用 `https:`。每个索引源属于当前 profile。 ## 浏览和安装 -工具栏左侧的市场菜单用于切换索引源,并可打开**管理索引源…**,其中每个索引源显示它最近一次的错误。搜索匹配名称、作者、关键词,以及所有语言的标题、描述、分类和标签,并容忍拼写错误;在你选择其他排序之前,结果按最佳匹配排序。**标签**菜单、分类按钮和**排序**菜单使用市场声明的分类、标签和排序键,**发布时间**按每个插件最新版本的时间排序。 +工具栏左侧的索引菜单用于切换索引源,并可打开**管理索引源…**,其中每个索引源显示它最近一次的错误。搜索匹配名称、作者、关键词,以及所有语言的标题、描述、分类和标签,并容忍拼写错误;在你选择其他排序之前,结果按最佳匹配排序。**标签**菜单、分类按钮和**排序**菜单使用索引声明的分类、标签和排序键,**发布时间**按每个插件最新版本的时间排序。 每张卡片显示插件的推荐版本:与当前运行的 DSH 兼容、且不是预发布版本的最高版本;插件没有列出任何非预发布版本时,则是兼容的最高预发布版本。**安装**会打开确认框,显示该版本的来源:npm 包和注册表,或 Git 仓库、提交和目录。确认后开始常规的插件安装,包括其中的兼容性检查和注册表检查;未经确认不会安装任何内容。 选择卡片会打开插件页面。**版本**列表显示列出的每个版本及其兼容性,并为 DSH 接受的任何版本提供**安装**,包括旧版本和预发布版本。显示**已安装**、**有新版本**或**随 DSH 提供**的卡片会改为打开已安装的组合包。 -市场是第三方内容。每个插件的标题、描述、图标、作者和来源都由市场决定;DSH 检查文件格式,但不验证这些声明。只添加你信任的索引源,并在安装前核对确认框中的来源。 +索引是第三方内容。每个插件的标题、描述、图标、作者和来源都由索引决定;DSH 检查文件格式,但不验证这些声明。只添加你信任的索引源,并在安装前核对确认框中的来源。 ## 发布索引 @@ -109,10 +109,10 @@ pnpm --filter @deepseek-ai/dsh-plugin-market-generator start --input plugins.txt | 索引字段 | 必填 | 含义 | |---|---|---| | `format` | 是 | `1`。 | -| `title`、`description` | 仅 `title` | 市场名称和描述,类型为 `LocalizedText`。 | +| `title`、`description` | 仅 `title` | 索引名称和描述,类型为 `LocalizedText`。 | | `metadata.categories`、`metadata.labels` | 否 | 将 id 映射为 `{ "title": LocalizedText }` 的对象。 | | `metadata.sortKeys` | 否 | 将 id 映射为 `{ "title": LocalizedText, "order": "asc" \| "desc" }` 的对象;`order` 是初始方向。 | -| `plugins` | 是 | 按市场默认顺序排列的插件。 | +| `plugins` | 是 | 按索引默认顺序排列的插件。 | | 插件字段 | 必填 | 含义 | |---|---|---| @@ -140,7 +140,7 @@ DSH 将 npm 版本安装为 `name@version`,存在 `registry` 时先询问它 ## 延伸阅读 - [「发现」标签页参考](../../../packages/client/ui-plugin-market/README.zh.md)说明该标签页的控件和状态。 -- [插件市场服务](../../../packages/boot/plugin-market/README.zh.md)说明获取限制、缓存和 `plugin_market` 工具。 +- [插件索引服务](../../../packages/boot/plugin-market/README.zh.md)说明获取限制、缓存和 `plugin_market` 工具。 - [索引校验器](../../../packages/util/plugin-market-index/README.zh.md)是生成器可以用来在发布前检查索引的库。 ## 开发备注 diff --git a/docs/user/guide/providers.i18n.yaml b/docs/user/guide/providers.i18n.yaml index 93bc2cc11f..b04aada43b 100644 --- a/docs/user/guide/providers.i18n.yaml +++ b/docs/user/guide/providers.i18n.yaml @@ -6,8 +6,8 @@ en: b1631dcf25e805a1 zh: 8ded02e2ae87f00b /configure-models/configure-deepseek: - en: 2c6b665adc256b4e - zh: 2240e4c1573ebe9c + en: 44852da64a6a39ec + zh: 4c280e428f0fec08 /configure-models/add-a-third-party-provider: en: f6fb44a85dc5e4dd zh: 2b86ea4492c32ed7 diff --git a/docs/user/guide/providers.md b/docs/user/guide/providers.md index 144b33d835..fe831bcaf9 100644 --- a/docs/user/guide/providers.md +++ b/docs/user/guide/providers.md @@ -12,6 +12,8 @@ Open **Settings → Models**. The DeepSeek card exposes one API-key field; enter Keys are write-only. The page receives a redacted descriptor after saving, never the literal secret. The key is stored in `$DSH_HOME/.credentials.yaml`, while settings retain only its credential reference. +With a key saved and the default endpoint, DeepSeek's models follow the list the DeepSeek API publishes. The list loads when the models are first needed, refreshes in the background about every ten minutes while they are in use, and falls back to the built-in models when it cannot load. Editing **Models** under **Customized settings** keeps your own list instead and stops following the published one; **Restore defaults** follows it again and reloads it. While the published list is in use, the card shows the built-in models, so a newly published model appears only in the model picker. The [API-key provider reference](../../../packages/llm/llm-deepseek-api-key/README.md) owns the listing, fallback, and gating rules. + ## Add a third-party provider Choose **Add model provider**. The card opens on **Third-party model provider**: pick a provider dsh ships with — the list shows provider ids such as `anthropic`, `openai`, `moonshotai` for Kimi, or `zai` for GLM — enter its API key, and save. The installed catalog supplies the endpoint, protocol, and model list. diff --git a/docs/user/guide/providers.zh.md b/docs/user/guide/providers.zh.md index e20d1eb4aa..bb518c42b9 100644 --- a/docs/user/guide/providers.zh.md +++ b/docs/user/guide/providers.zh.md @@ -12,6 +12,8 @@ 密钥是只写的。保存后,页面只会收到脱敏描述符,永远不会收到明文密钥。密钥存储在 `$DSH_HOME/.credentials.yaml` 中,settings 只保留它的凭据引用。 +保存了密钥且使用默认端点时,DeepSeek 的模型跟随 DeepSeek API 发布的列表。第一次需要模型时加载列表,使用期间约每十分钟在后台刷新一次,无法加载时退回内置模型。在**自定义设置**里编辑**模型目录**会改用你自己的列表,不再跟随发布的列表;点**恢复默认模型**会重新跟随并重新加载。使用发布的列表时,卡片上显示的是内置模型,因此新发布的模型只出现在模型选择器中。列表、兜底和门控规则见 [API key 提供方参考](../../../packages/llm/llm-deepseek-api-key/README.zh.md)。 + ## 添加第三方提供商 选择**添加模型提供商**。卡片默认打开在**第三方模型提供商**:选取 dsh 自带的提供商——列表显示的是提供商 id,例如 `anthropic`、`openai`、Kimi 对应的 `moonshotai`、GLM 对应的 `zai`——输入其 API 密钥并保存。已安装目录会提供端点、协议和模型列表。 diff --git a/packages/acp/acp/locale/de.json b/packages/acp/acp/locale/de.json new file mode 100644 index 0000000000..1d616ae6f6 --- /dev/null +++ b/packages/acp/acp/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ACP-Server", + "description": "Führen Sie Agenten zur Automatisierung über JSON-RPC über Standardeingabe und -ausgabe aus." + } +} diff --git a/packages/acp/acp/locale/es.json b/packages/acp/acp/locale/es.json new file mode 100644 index 0000000000..4970a451fb --- /dev/null +++ b/packages/acp/acp/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "servidor ACP", + "description": "Ejecute agentes a través de JSON-RPC a través de entrada y salida estándar para automatización." + } +} diff --git a/packages/acp/acp/locale/fr.json b/packages/acp/acp/locale/fr.json new file mode 100644 index 0000000000..a8faaceeaa --- /dev/null +++ b/packages/acp/acp/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Serveur ACP", + "description": "Exécutez les agents via JSON-RPC via une entrée et une sortie standard pour l'automatisation." + } +} diff --git a/packages/acp/acp/locale/pt.json b/packages/acp/acp/locale/pt.json new file mode 100644 index 0000000000..cad2b3ae48 --- /dev/null +++ b/packages/acp/acp/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ACP servidor", + "description": "Execute agentes por meio de JSON-RPC em entradas e saídas padrão para automação." + } +} diff --git a/packages/acp/acp/locale/ru.json b/packages/acp/acp/locale/ru.json new file mode 100644 index 0000000000..fefd7d956d --- /dev/null +++ b/packages/acp/acp/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ACP сервер", + "description": "Запускайте агенты через JSON-RPC через стандартный ввод и вывод для автоматизации." + } +} diff --git a/packages/api/account-controller/locale/de.json b/packages/api/account-controller/locale/de.json new file mode 100644 index 0000000000..f1e7b8ca6c --- /dev/null +++ b/packages/api/account-controller/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Benutzeroberfläche für den Kontobetrieb", + "description": "Bieten Sie authentifizierten Clients Kontoverwaltungsfunktionen." + } +} diff --git a/packages/api/account-controller/locale/es.json b/packages/api/account-controller/locale/es.json new file mode 100644 index 0000000000..7bfaac6431 --- /dev/null +++ b/packages/api/account-controller/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de operación de cuenta", + "description": "Proporcionar capacidades de operación de cuentas a clientes autenticados." + } +} diff --git a/packages/api/account-controller/locale/fr.json b/packages/api/account-controller/locale/fr.json new file mode 100644 index 0000000000..a77bd7e7a1 --- /dev/null +++ b/packages/api/account-controller/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface d'opération de compte", + "description": "Fournir des capacités d’exploitation de compte aux clients authentifiés." + } +} diff --git a/packages/api/account-controller/locale/pt.json b/packages/api/account-controller/locale/pt.json new file mode 100644 index 0000000000..22670a3e3b --- /dev/null +++ b/packages/api/account-controller/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de operação da conta", + "description": "Fornece recursos de operação de conta para clientes autenticados." + } +} diff --git a/packages/api/account-controller/locale/ru.json b/packages/api/account-controller/locale/ru.json new file mode 100644 index 0000000000..dd3e515e30 --- /dev/null +++ b/packages/api/account-controller/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс работы со счетом", + "description": "Предоставьте возможности управления учетной записью аутентифицированным клиентам." + } +} diff --git a/packages/api/gateway/locale/de.json b/packages/api/gateway/locale/de.json new file mode 100644 index 0000000000..f2fd7153f5 --- /dev/null +++ b/packages/api/gateway/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Remote-Call-Gateway", + "description": "Empfangen Sie den Schnittstellenaufruf des Clients und verteilen Sie ihn an den entsprechenden Dienst." + } +} diff --git a/packages/api/gateway/locale/es.json b/packages/api/gateway/locale/es.json new file mode 100644 index 0000000000..7c02c79a8e --- /dev/null +++ b/packages/api/gateway/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "puerta de enlace de llamada remota", + "description": "Recibir la llamada de interfaz del cliente y distribuirla al servicio correspondiente." + } +} diff --git a/packages/api/gateway/locale/fr.json b/packages/api/gateway/locale/fr.json new file mode 100644 index 0000000000..2eec7623b4 --- /dev/null +++ b/packages/api/gateway/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "passerelle d'appel à distance", + "description": "Recevez l'appel d'interface du client et distribuez-le au service correspondant." + } +} diff --git a/packages/api/gateway/locale/pt.json b/packages/api/gateway/locale/pt.json new file mode 100644 index 0000000000..856eba1312 --- /dev/null +++ b/packages/api/gateway/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "gateway de chamada remota", + "description": "Receba a chamada de interface do cliente e distribua-a ao serviço correspondente." + } +} diff --git a/packages/api/gateway/locale/ru.json b/packages/api/gateway/locale/ru.json new file mode 100644 index 0000000000..7e7cf69bd3 --- /dev/null +++ b/packages/api/gateway/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "шлюз удаленных вызовов", + "description": "Получите вызов интерфейса клиента и передайте его соответствующему сервису." + } +} diff --git a/packages/api/job-controller/locale/de.json b/packages/api/job-controller/locale/de.json new file mode 100644 index 0000000000..ffddc2d9c6 --- /dev/null +++ b/packages/api/job-controller/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zur Beobachtung von Hintergrundaufgaben", + "description": "Stellen Sie dem Kunden kontinuierlich den Status und die Ausgabe von Hintergrundaufgaben zur Verfügung." + } +} diff --git a/packages/api/job-controller/locale/es.json b/packages/api/job-controller/locale/es.json new file mode 100644 index 0000000000..d7fbfdc02e --- /dev/null +++ b/packages/api/job-controller/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de observación de tareas en segundo plano", + "description": "Proporcionar continuamente el estado y la salida de las tareas en segundo plano al cliente." + } +} diff --git a/packages/api/job-controller/locale/fr.json b/packages/api/job-controller/locale/fr.json new file mode 100644 index 0000000000..aad494710a --- /dev/null +++ b/packages/api/job-controller/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface d'observation des tâches en arrière-plan", + "description": "Fournir en permanence l'état et la sortie des tâches en arrière-plan au client." + } +} diff --git a/packages/api/job-controller/locale/pt.json b/packages/api/job-controller/locale/pt.json new file mode 100644 index 0000000000..c98a63561b --- /dev/null +++ b/packages/api/job-controller/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de observação de tarefas em segundo plano", + "description": "Fornece continuamente o status e a saída de tarefas em segundo plano ao cliente." + } +} diff --git a/packages/api/job-controller/locale/ru.json b/packages/api/job-controller/locale/ru.json new file mode 100644 index 0000000000..60ecb46b94 --- /dev/null +++ b/packages/api/job-controller/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс наблюдения за фоновыми задачами", + "description": "Постоянно предоставляйте клиенту статус и вывод фоновых задач." + } +} diff --git a/packages/api/remote-access/locale/de.json b/packages/api/remote-access/locale/de.json new file mode 100644 index 0000000000..14b83f143c --- /dev/null +++ b/packages/api/remote-access/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Fernzugriffskontrolle", + "description": "Steuern Sie, mit welchen Methoden Nicht-Eigentümer-Verbindungen auf Berechtigungs-Snapshots für jede Verbindung zugreifen und diese beibehalten können." + } +} diff --git a/packages/api/remote-access/locale/es.json b/packages/api/remote-access/locale/es.json new file mode 100644 index 0000000000..dda54e1529 --- /dev/null +++ b/packages/api/remote-access/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "control de acceso remoto", + "description": "Controle a qué métodos pueden acceder las conexiones que no son propietarios y retener instantáneas de permisos para cada conexión." + } +} diff --git a/packages/api/remote-access/locale/fr.json b/packages/api/remote-access/locale/fr.json new file mode 100644 index 0000000000..96082a9342 --- /dev/null +++ b/packages/api/remote-access/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Contrôle d'accès à distance", + "description": "Contrôlez les méthodes auxquelles les connexions non propriétaires peuvent accéder et conserver les instantanés d'autorisation pour chaque connexion." + } +} diff --git a/packages/api/remote-access/locale/pt.json b/packages/api/remote-access/locale/pt.json new file mode 100644 index 0000000000..840eaf556f --- /dev/null +++ b/packages/api/remote-access/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Controle de acesso remoto", + "description": "Controle quais métodos as conexões não proprietárias podem acessar e reter instantâneos de permissão para cada conexão." + } +} diff --git a/packages/api/remote-access/locale/ru.json b/packages/api/remote-access/locale/ru.json new file mode 100644 index 0000000000..1faa15dd38 --- /dev/null +++ b/packages/api/remote-access/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Управление удаленным доступом", + "description": "Контролируйте, какие методы соединения, не являющиеся владельцами, могут получать доступ и сохранять снимки разрешений для каждого соединения." + } +} diff --git a/packages/api/remotes/locale/de.json b/packages/api/remotes/locale/de.json new file mode 100644 index 0000000000..dcc7ff1da3 --- /dev/null +++ b/packages/api/remotes/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Zusammenfassung des Kunden API", + "description": "Fasst die Serverfunktionen zusammen, die Clients durch Anwendungen zur Verfügung stehen." + } +} diff --git a/packages/api/remotes/locale/es.json b/packages/api/remotes/locale/es.json new file mode 100644 index 0000000000..d731b0c788 --- /dev/null +++ b/packages/api/remotes/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Resumen del cliente API", + "description": "Resume las capacidades del servidor abiertas a los clientes por las aplicaciones." + } +} diff --git a/packages/api/remotes/locale/fr.json b/packages/api/remotes/locale/fr.json new file mode 100644 index 0000000000..a85835665d --- /dev/null +++ b/packages/api/remotes/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Récapitulatif du client API", + "description": "Résume les fonctionnalités du serveur ouvertes aux clients par les applications." + } +} diff --git a/packages/api/remotes/locale/pt.json b/packages/api/remotes/locale/pt.json new file mode 100644 index 0000000000..9bc13b83ff --- /dev/null +++ b/packages/api/remotes/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Resumo do cliente API", + "description": "Resume os recursos do servidor abertos aos clientes pelos aplicativos." + } +} diff --git a/packages/api/remotes/locale/ru.json b/packages/api/remotes/locale/ru.json new file mode 100644 index 0000000000..804cf94148 --- /dev/null +++ b/packages/api/remotes/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Сводная информация о клиенте API", + "description": "Обобщает возможности сервера, открытые для клиентов приложениями." + } +} diff --git a/packages/api/session-controller/locale/de.json b/packages/api/session-controller/locale/de.json new file mode 100644 index 0000000000..217f0baf00 --- /dev/null +++ b/packages/api/session-controller/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Sitzungssteuerungsschnittstelle", + "description": "Bietet Clients Funktionen zum Lesen von Sitzungen, zur Laufsteuerung und zur Statusaktualisierung." + } +} diff --git a/packages/api/session-controller/locale/es.json b/packages/api/session-controller/locale/es.json new file mode 100644 index 0000000000..764c28e00b --- /dev/null +++ b/packages/api/session-controller/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "interfaz de control de sesión", + "description": "Proporciona capacidades de lectura de sesiones, control de ejecución y actualización de estado a los clientes." + } +} diff --git a/packages/api/session-controller/locale/fr.json b/packages/api/session-controller/locale/fr.json new file mode 100644 index 0000000000..689e058b56 --- /dev/null +++ b/packages/api/session-controller/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "interface de contrôle de session", + "description": "Fournit des fonctionnalités de lecture de session, de contrôle d’exécution et de mise à jour de statut aux clients." + } +} diff --git a/packages/api/session-controller/locale/pt.json b/packages/api/session-controller/locale/pt.json new file mode 100644 index 0000000000..537a8f934d --- /dev/null +++ b/packages/api/session-controller/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "interface de controle de sessão", + "description": "Fornece leitura de sessão, controle de execução e recursos de atualização de status aos clientes." + } +} diff --git a/packages/api/session-controller/locale/ru.json b/packages/api/session-controller/locale/ru.json new file mode 100644 index 0000000000..885910a705 --- /dev/null +++ b/packages/api/session-controller/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "интерфейс управления сеансом", + "description": "Предоставляет клиентам возможности чтения сеанса, управления запуском и обновления статуса." + } +} diff --git a/packages/api/settings-controller/locale/de.json b/packages/api/settings-controller/locale/de.json new file mode 100644 index 0000000000..f27d4899d3 --- /dev/null +++ b/packages/api/settings-controller/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Lese- und Schreibschnittstelle einrichten", + "description": "Bieten Sie dem Kunden die Möglichkeit, verschiedene Einstellungen zu lesen und zu ändern." + } +} diff --git a/packages/api/settings-controller/locale/es.json b/packages/api/settings-controller/locale/es.json new file mode 100644 index 0000000000..39d21f44e7 --- /dev/null +++ b/packages/api/settings-controller/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurar la interfaz de lectura y escritura", + "description": "Proporcionar al cliente la capacidad de leer y modificar varias configuraciones." + } +} diff --git a/packages/api/settings-controller/locale/fr.json b/packages/api/settings-controller/locale/fr.json new file mode 100644 index 0000000000..7fe6fcd1d9 --- /dev/null +++ b/packages/api/settings-controller/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurer l'interface de lecture et d'écriture", + "description": "Offrir au client la possibilité de lire et de modifier divers paramètres." + } +} diff --git a/packages/api/settings-controller/locale/pt.json b/packages/api/settings-controller/locale/pt.json new file mode 100644 index 0000000000..46507f0ae7 --- /dev/null +++ b/packages/api/settings-controller/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurar interface de leitura e gravação", + "description": "Forneça ao cliente a capacidade de ler e modificar várias configurações." + } +} diff --git a/packages/api/settings-controller/locale/ru.json b/packages/api/settings-controller/locale/ru.json new file mode 100644 index 0000000000..a24ae67e2d --- /dev/null +++ b/packages/api/settings-controller/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Настройка интерфейса чтения и записи", + "description": "Предоставьте клиенту возможность читать и изменять различные настройки." + } +} diff --git a/packages/api/terminal-controller/locale/de.json b/packages/api/terminal-controller/locale/de.json new file mode 100644 index 0000000000..40a84f00ec --- /dev/null +++ b/packages/api/terminal-controller/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Terminal-Steuerungsschnittstelle", + "description": "Bietet Clients die Möglichkeit, Sitzungsterminals zu erstellen, einzugeben, zu lesen und wiederherzustellen." + } +} diff --git a/packages/api/terminal-controller/locale/es.json b/packages/api/terminal-controller/locale/es.json new file mode 100644 index 0000000000..89251b9b70 --- /dev/null +++ b/packages/api/terminal-controller/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "interfaz de control terminal", + "description": "Proporciona a los clientes la capacidad de crear, ingresar, leer y restaurar terminales de sesión." + } +} diff --git a/packages/api/terminal-controller/locale/fr.json b/packages/api/terminal-controller/locale/fr.json new file mode 100644 index 0000000000..e016120a32 --- /dev/null +++ b/packages/api/terminal-controller/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "interface de contrôle des terminaux", + "description": "Fournit aux clients la possibilité de créer, saisir, lire et restaurer des terminaux de session." + } +} diff --git a/packages/api/terminal-controller/locale/pt.json b/packages/api/terminal-controller/locale/pt.json new file mode 100644 index 0000000000..3e2aa9afb6 --- /dev/null +++ b/packages/api/terminal-controller/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "interface de controle de terminal", + "description": "Fornece aos clientes a capacidade de criar, inserir, ler e restaurar terminais de sessão." + } +} diff --git a/packages/api/terminal-controller/locale/ru.json b/packages/api/terminal-controller/locale/ru.json new file mode 100644 index 0000000000..c2c8651003 --- /dev/null +++ b/packages/api/terminal-controller/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "интерфейс управления терминалом", + "description": "Предоставляет клиентам возможность создавать, вводить, читать и восстанавливать сеансовые терминалы." + } +} diff --git a/packages/api/workspace-controller/locale/de.json b/packages/api/workspace-controller/locale/de.json new file mode 100644 index 0000000000..f36dfe99d3 --- /dev/null +++ b/packages/api/workspace-controller/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zur Arbeitsbereichssteuerung", + "description": "Bietet Clients Arbeitsbereichsvorgänge und einen Status, der bei erneuter Verbindung wiederhergestellt werden kann." + } +} diff --git a/packages/api/workspace-controller/locale/es.json b/packages/api/workspace-controller/locale/es.json new file mode 100644 index 0000000000..3a451aba01 --- /dev/null +++ b/packages/api/workspace-controller/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de control del espacio de trabajo", + "description": "Proporciona a los clientes operaciones en el espacio de trabajo y un estado que se puede restaurar tras la reconexión." + } +} diff --git a/packages/api/workspace-controller/locale/fr.json b/packages/api/workspace-controller/locale/fr.json new file mode 100644 index 0000000000..d6b5f2a527 --- /dev/null +++ b/packages/api/workspace-controller/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de contrôle de l'espace de travail", + "description": "Fournit aux clients des opérations d’espace de travail et un état qui peut être restauré lors de la reconnexion." + } +} diff --git a/packages/api/workspace-controller/locale/pt.json b/packages/api/workspace-controller/locale/pt.json new file mode 100644 index 0000000000..0564e0743f --- /dev/null +++ b/packages/api/workspace-controller/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de controle do espaço de trabalho", + "description": "Fornece aos clientes operações de espaço de trabalho e um estado que pode ser restaurado após a reconexão." + } +} diff --git a/packages/api/workspace-controller/locale/ru.json b/packages/api/workspace-controller/locale/ru.json new file mode 100644 index 0000000000..19da086fb1 --- /dev/null +++ b/packages/api/workspace-controller/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс управления рабочей областью", + "description": "Предоставляет клиентам операции в рабочей области и состояние, которое можно восстановить при повторном подключении." + } +} diff --git a/packages/api/workspace-files/locale/de.json b/packages/api/workspace-files/locale/de.json new file mode 100644 index 0000000000..60f31b3a27 --- /dev/null +++ b/packages/api/workspace-files/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Workspace-Dateischnittstelle", + "description": "Bietet das Lesen von Arbeitsbereichsdateien, das Auflisten von Verzeichnissen und die Aktualisierung von Dateiinformationen." + } +} diff --git a/packages/api/workspace-files/locale/es.json b/packages/api/workspace-files/locale/es.json new file mode 100644 index 0000000000..91db1da1f2 --- /dev/null +++ b/packages/api/workspace-files/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de archivos del espacio de trabajo", + "description": "Proporciona lectura de archivos del espacio de trabajo, listado de directorios y actualizaciones de información de archivos." + } +} diff --git a/packages/api/workspace-files/locale/fr.json b/packages/api/workspace-files/locale/fr.json new file mode 100644 index 0000000000..490d88dcbf --- /dev/null +++ b/packages/api/workspace-files/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de fichier de l'espace de travail", + "description": "Fournit la lecture des fichiers de l'espace de travail, la liste des répertoires et les mises à jour des informations sur les fichiers." + } +} diff --git a/packages/api/workspace-files/locale/pt.json b/packages/api/workspace-files/locale/pt.json new file mode 100644 index 0000000000..e0d49a666d --- /dev/null +++ b/packages/api/workspace-files/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de arquivo do espaço de trabalho", + "description": "Fornece leitura de arquivos do espaço de trabalho, listagem de diretórios e atualizações de informações de arquivos." + } +} diff --git a/packages/api/workspace-files/locale/ru.json b/packages/api/workspace-files/locale/ru.json new file mode 100644 index 0000000000..3d514ad55b --- /dev/null +++ b/packages/api/workspace-files/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Файловый интерфейс рабочей области", + "description": "Обеспечивает чтение файлов рабочей области, список каталогов и обновление информации о файлах." + } +} diff --git a/packages/attachment/attachment-local/locale/de.json b/packages/attachment/attachment-local/locale/de.json new file mode 100644 index 0000000000..1095b25bb2 --- /dev/null +++ b/packages/attachment/attachment-local/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Lokaler Anhangsspeicher", + "description": "Speichern Sie Anhänge wie Bilder in der Sitzung und lesen Sie sie entsprechend der Inhaltskennung." + } +} diff --git a/packages/attachment/attachment-local/locale/es.json b/packages/attachment/attachment-local/locale/es.json new file mode 100644 index 0000000000..59ca4b5d35 --- /dev/null +++ b/packages/attachment/attachment-local/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Almacenamiento de archivos adjuntos locales", + "description": "Guarde archivos adjuntos como imágenes de la sesión y léalos según el identificador de contenido." + } +} diff --git a/packages/attachment/attachment-local/locale/fr.json b/packages/attachment/attachment-local/locale/fr.json new file mode 100644 index 0000000000..cb5019bfc5 --- /dev/null +++ b/packages/attachment/attachment-local/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Stockage local des pièces jointes", + "description": "Enregistrez les pièces jointes telles que les images dans la session et lisez-les en fonction de l'identifiant du contenu." + } +} diff --git a/packages/attachment/attachment-local/locale/pt.json b/packages/attachment/attachment-local/locale/pt.json new file mode 100644 index 0000000000..030a96aed5 --- /dev/null +++ b/packages/attachment/attachment-local/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Armazenamento local de anexos", + "description": "Salve anexos como fotos na sessão e leia-os de acordo com o identificador de conteúdo." + } +} diff --git a/packages/attachment/attachment-local/locale/ru.json b/packages/attachment/attachment-local/locale/ru.json new file mode 100644 index 0000000000..d452d5a8f5 --- /dev/null +++ b/packages/attachment/attachment-local/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Локальное хранилище вложений", + "description": "Сохраняйте вложения, например изображения, в сеансе и читайте их в соответствии с идентификатором контента." + } +} diff --git a/packages/boot/README.i18n.yaml b/packages/boot/README.i18n.yaml index 8ecc1fc6e8..1c4b79c79d 100644 --- a/packages/boot/README.i18n.yaml +++ b/packages/boot/README.i18n.yaml @@ -15,8 +15,8 @@ en: 09848f4d056e4fc1 zh: 8df6139cff0288f6 /boot-shared-app-bin-boot-glue/packages: - en: 71736c39fd71952c - zh: 2033a18debd8a2d8 + en: c97ae01c92f6756e + zh: 2ebb832078874b7a /boot-shared-app-bin-boot-glue/related-documentation: en: 77af4b40ee7b98a3 zh: e9b4e817823b973d diff --git a/packages/boot/README.md b/packages/boot/README.md index d3d6610d1c..9ace015e65 100644 --- a/packages/boot/README.md +++ b/packages/boot/README.md @@ -28,7 +28,7 @@ The boot group launches profile applications and manages their installed composi | [`hmr`](hmr/README.md) | Coordinates module and configuration reloads with package mutations | `hmr` | | [`config-editor`](config-editor/README.md) | Persists active profile configuration through Loader reconciliation | `configEditor` | | [`plugin-manager`](plugin-manager/README.md) | Manages current-profile plugins and bundle packages through shared CLI operations | `pluginManager` | -| [`plugin-market`](plugin-market/README.md) | Reads the plugin market indexes the user configures for Discover and the `plugin_market` tool | `pluginMarket` | +| [`plugin-market`](plugin-market/README.md) | Reads the plugin indexes the user configures for Discover and the `plugin_market` tool | `pluginMarket` | ## Related documentation diff --git a/packages/boot/README.zh.md b/packages/boot/README.zh.md index 12c9b32d7d..aa0c6d3d37 100644 --- a/packages/boot/README.zh.md +++ b/packages/boot/README.zh.md @@ -28,7 +28,7 @@ boot 组负责启动 profile 应用并管理其已安装组合。`app-boot` 解 | [`hmr`](hmr/README.zh.md) | 协调模块与配置重载,并与包修改互斥执行 | `hmr` | | [`config-editor`](config-editor/README.zh.md) | 通过 Loader 应用并持久化当前 profile 配置 | `configEditor` | | [`plugin-manager`](plugin-manager/README.zh.md) | 通过共享 CLI 操作管理当前 profile 插件与组合包 | `pluginManager` | -| [`plugin-market`](plugin-market/README.zh.md) | 读取用户配置的插件市场索引,供发现页和 `plugin_market` 工具使用 | `pluginMarket` | +| [`plugin-market`](plugin-market/README.zh.md) | 读取用户配置的插件索引,供发现页和 `plugin_market` 工具使用 | `pluginMarket` | ## 相关文档 diff --git a/packages/boot/config-editor/locale/de.json b/packages/boot/config-editor/locale/de.json new file mode 100644 index 0000000000..d72e2c7250 --- /dev/null +++ b/packages/boot/config-editor/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Plug-in-Konfigurationseditor", + "description": "Speichern Sie die Plug-in-Konfiguration und übernehmen Sie Änderungen auf die aktuell ausgeführte Konfiguration." + } +} diff --git a/packages/boot/config-editor/locale/es.json b/packages/boot/config-editor/locale/es.json new file mode 100644 index 0000000000..a2448c1f5d --- /dev/null +++ b/packages/boot/config-editor/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Editor de configuración de complementos", + "description": "Guarde la configuración del complemento y aplique los cambios a la configuración en ejecución actual." + } +} diff --git a/packages/boot/config-editor/locale/fr.json b/packages/boot/config-editor/locale/fr.json new file mode 100644 index 0000000000..f2b3eb621d --- /dev/null +++ b/packages/boot/config-editor/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Éditeur de configuration de plug-in", + "description": "Enregistrez la configuration du plug-in et appliquez les modifications à la configuration en cours d'exécution." + } +} diff --git a/packages/boot/config-editor/locale/pt.json b/packages/boot/config-editor/locale/pt.json new file mode 100644 index 0000000000..8f2a80a8aa --- /dev/null +++ b/packages/boot/config-editor/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Editor de configuração de plug-ins", + "description": "Salve a configuração do plug-in e aplique as alterações à configuração em execução atual." + } +} diff --git a/packages/boot/config-editor/locale/ru.json b/packages/boot/config-editor/locale/ru.json new file mode 100644 index 0000000000..b14ff9c92c --- /dev/null +++ b/packages/boot/config-editor/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Редактор конфигурации плагина", + "description": "Сохраните конфигурацию плагина и примените изменения к текущей работающей конфигурации." + } +} diff --git a/packages/boot/hmr/locale/de.json b/packages/boot/hmr/locale/de.json new file mode 100644 index 0000000000..45ffdf967a --- /dev/null +++ b/packages/boot/hmr/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Hmr", + "description": "Laden Sie den Plugin-Code und die Konfiguration neu, während die App ausgeführt wird." + } +} diff --git a/packages/boot/hmr/locale/es.json b/packages/boot/hmr/locale/es.json new file mode 100644 index 0000000000..06af2d1b51 --- /dev/null +++ b/packages/boot/hmr/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "hmr", + "description": "Vuelva a cargar el código y la configuración del complemento mientras la aplicación se está ejecutando." + } +} diff --git a/packages/boot/hmr/locale/fr.json b/packages/boot/hmr/locale/fr.json new file mode 100644 index 0000000000..096306a34e --- /dev/null +++ b/packages/boot/hmr/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "hum", + "description": "Rechargez le code et la configuration du plugin pendant que l'application est en cours d'exécution." + } +} diff --git a/packages/boot/hmr/locale/pt.json b/packages/boot/hmr/locale/pt.json new file mode 100644 index 0000000000..b9ee71fba2 --- /dev/null +++ b/packages/boot/hmr/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "hum", + "description": "Recarregue o código e a configuração do plugin enquanto o aplicativo estiver em execução." + } +} diff --git a/packages/boot/hmr/locale/ru.json b/packages/boot/hmr/locale/ru.json new file mode 100644 index 0000000000..0ed5dc0fdc --- /dev/null +++ b/packages/boot/hmr/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "хмр", + "description": "Перезагрузите код и конфигурацию плагина во время работы приложения." + } +} diff --git a/packages/boot/official-bundles/src/catalog.generated.ts b/packages/boot/official-bundles/src/catalog.generated.ts index 52be77c5ca..8a0cfb7579 100644 --- a/packages/boot/official-bundles/src/catalog.generated.ts +++ b/packages/boot/official-bundles/src/catalog.generated.ts @@ -7,16 +7,16 @@ export const OFFICIAL_ON_DEMAND_CATALOG: readonly OfficialBundleCatalogEntry[] = { packageName: '@deepseek-ai/dsh-subagent-claude-code', meta: { - title: { 'en': 'Claude Code subagent', 'ja': 'Claude Code \u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8', 'zh': 'Claude Code \u5B50\u667A\u80FD\u4F53' }, - description: { 'en': 'Delegate one task to Claude Code using its native authentication and settings.', 'ja': 'Claude Code \u306E\u30CD\u30A4\u30C6\u30A3\u30D6\u306E\u8A8D\u8A3C\u3068\u8A2D\u5B9A\u3092\u4F7F\u7528\u3057\u3066\u3001\u5358\u72EC\u306E\u30BF\u30B9\u30AF\u3092\u59D4\u6D3E\u3057\u307E\u3059\u3002', 'zh': '\u4F7F\u7528 Claude Code \u539F\u751F\u8BA4\u8BC1\u548C\u8BBE\u7F6E\u59D4\u6D3E\u5355\u4E2A\u4EFB\u52A1\u3002' }, + title: { 'de': 'Claude Code-Subagent', 'en': 'Claude Code subagent', 'es': 'Subagente Claude Code', 'fr': 'Sous-agent Claude Code', 'ja': 'Claude Code \u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8', 'pt': 'Claude Code subagente', 'ru': '\u0421\u0443\u0431\u0430\u0433\u0435\u043D\u0442 Claude Code', 'zh': 'Claude Code \u5B50\u667A\u80FD\u4F53' }, + description: { 'de': 'Delegieren Sie eine Aufgabe an Claude Code, indem Sie dessen native Authentifizierung und Einstellungen verwenden.', 'en': 'Delegate one task to Claude Code using its native authentication and settings.', 'es': 'Delegue una tarea a Claude Code utilizando su autenticaci\u00F3n y configuraci\u00F3n nativas.', 'fr': 'D\u00E9l\u00E9guez une t\u00E2che \u00E0 Claude Code \u00E0 l\u2019aide de son authentification et de ses param\u00E8tres natifs.', 'ja': 'Claude Code \u306E\u30CD\u30A4\u30C6\u30A3\u30D6\u306E\u8A8D\u8A3C\u3068\u8A2D\u5B9A\u3092\u4F7F\u7528\u3057\u3066\u3001\u5358\u72EC\u306E\u30BF\u30B9\u30AF\u3092\u59D4\u6D3E\u3057\u307E\u3059\u3002', 'pt': 'Delegue uma tarefa a Claude Code usando sua autentica\u00E7\u00E3o e configura\u00E7\u00F5es nativas.', 'ru': '\u0414\u0435\u043B\u0435\u0433\u0438\u0440\u0443\u0439\u0442\u0435 \u043E\u0434\u043D\u0443 \u0437\u0430\u0434\u0430\u0447\u0443 Claude Code, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u044F \u0435\u0433\u043E \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u0443\u044E \u0430\u0443\u0442\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044E \u0438 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438.', 'zh': '\u4F7F\u7528 Claude Code \u539F\u751F\u8BA4\u8BC1\u548C\u8BBE\u7F6E\u59D4\u6D3E\u5355\u4E2A\u4EFB\u52A1\u3002' }, icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSIxNC45ODkzIiB5PSIxNS4xODc3IiB3aWR0aD0iMTIuMjM2NSIgaGVpZ2h0PSIxMi4yMzY1IiByeD0iMiIgZmlsbD0idXJsKCNkc2hfcGx1Z2luX2FydF9SMGEpIiBmaWxsLW9wYWNpdHk9IjAuOCI+PC9yZWN0PjxyZWN0IHg9IjguODcxMDkiIHk9IjkuMDY5NyIgd2lkdGg9IjEyLjIzNjUiIGhlaWdodD0iMTIuMjM2NSIgcng9IjIiIGZpbGw9InVybCgjZHNoX3BsdWdpbl9hcnRfUjBiKSIgZmlsbC1vcGFjaXR5PSIwLjgiPjwvcmVjdD48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYSIgeDE9IjIxLjEwNzUiIHkxPSIxNS4xODc3IiB4Mj0iMjEuMTA3NSIgeTI9IjI3LjQyNDMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjNDVFN0E0Ij48L3N0b3A+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDU5MDlEIj48L3N0b3A+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYiIgeDE9IjE0Ljk4OTQiIHkxPSI5LjA2OTciIHgyPSIxNC45ODk0IiB5Mj0iMjEuMzA2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiM2OUI5RkYiPjwvc3RvcD48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMjRERTIiPjwvc3RvcD48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48L3N2Zz4K', }, }, { packageName: '@deepseek-ai/dsh-subagent-codex', meta: { - title: { 'en': 'Codex subagent', 'ja': 'Codex \u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8', 'zh': 'Codex \u5B50\u667A\u80FD\u4F53' }, - description: { 'en': 'Delegate one task to Codex using its native authentication and settings.', 'ja': 'Codex \u306E\u30CD\u30A4\u30C6\u30A3\u30D6\u306E\u8A8D\u8A3C\u3068\u8A2D\u5B9A\u3092\u4F7F\u7528\u3057\u3066\u3001\u5358\u72EC\u306E\u30BF\u30B9\u30AF\u3092\u59D4\u6D3E\u3057\u307E\u3059\u3002', 'zh': '\u4F7F\u7528 Codex \u539F\u751F\u8BA4\u8BC1\u548C\u8BBE\u7F6E\u59D4\u6D3E\u5355\u4E2A\u4EFB\u52A1\u3002' }, + title: { 'de': 'Codex-Subagent', 'en': 'Codex subagent', 'es': 'Subagente Codex', 'fr': 'Sous-agent Codex', 'ja': 'Codex \u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8', 'pt': 'Codex subagente', 'ru': '\u0421\u0443\u0431\u0430\u0433\u0435\u043D\u0442 Codex', 'zh': 'Codex \u5B50\u667A\u80FD\u4F53' }, + description: { 'de': 'Delegieren Sie eine Aufgabe an Codex, indem Sie dessen native Authentifizierung und Einstellungen verwenden.', 'en': 'Delegate one task to Codex using its native authentication and settings.', 'es': 'Delegue una tarea a Codex utilizando su autenticaci\u00F3n y configuraci\u00F3n nativas.', 'fr': 'D\u00E9l\u00E9guez une t\u00E2che \u00E0 Codex \u00E0 l\u2019aide de son authentification et de ses param\u00E8tres natifs.', 'ja': 'Codex \u306E\u30CD\u30A4\u30C6\u30A3\u30D6\u306E\u8A8D\u8A3C\u3068\u8A2D\u5B9A\u3092\u4F7F\u7528\u3057\u3066\u3001\u5358\u72EC\u306E\u30BF\u30B9\u30AF\u3092\u59D4\u6D3E\u3057\u307E\u3059\u3002', 'pt': 'Delegue uma tarefa a Codex usando sua autentica\u00E7\u00E3o e configura\u00E7\u00F5es nativas.', 'ru': '\u0414\u0435\u043B\u0435\u0433\u0438\u0440\u0443\u0439\u0442\u0435 \u043E\u0434\u043D\u0443 \u0437\u0430\u0434\u0430\u0447\u0443 Codex, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u044F \u0435\u0433\u043E \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u0443\u044E \u0430\u0443\u0442\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044E \u0438 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438.', 'zh': '\u4F7F\u7528 Codex \u539F\u751F\u8BA4\u8BC1\u548C\u8BBE\u7F6E\u59D4\u6D3E\u5355\u4E2A\u4EFB\u52A1\u3002' }, icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSIxNC45ODkzIiB5PSIxNS4xODc3IiB3aWR0aD0iMTIuMjM2NSIgaGVpZ2h0PSIxMi4yMzY1IiByeD0iMiIgZmlsbD0idXJsKCNkc2hfcGx1Z2luX2FydF9SMGEpIiBmaWxsLW9wYWNpdHk9IjAuOCI+PC9yZWN0PjxyZWN0IHg9IjguODcxMDkiIHk9IjkuMDY5NyIgd2lkdGg9IjEyLjIzNjUiIGhlaWdodD0iMTIuMjM2NSIgcng9IjIiIGZpbGw9InVybCgjZHNoX3BsdWdpbl9hcnRfUjBiKSIgZmlsbC1vcGFjaXR5PSIwLjgiPjwvcmVjdD48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYSIgeDE9IjIxLjEwNzUiIHkxPSIxNS4xODc3IiB4Mj0iMjEuMTA3NSIgeTI9IjI3LjQyNDMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjNDVFN0E0Ij48L3N0b3A+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDU5MDlEIj48L3N0b3A+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYiIgeDE9IjE0Ljk4OTQiIHkxPSI5LjA2OTciIHgyPSIxNC45ODk0IiB5Mj0iMjEuMzA2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiM2OUI5RkYiPjwvc3RvcD48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMjRERTIiPjwvc3RvcD48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48L3N2Zz4K', }, }, diff --git a/packages/boot/plugin-manager/locale/de.json b/packages/boot/plugin-manager/locale/de.json new file mode 100644 index 0000000000..d9a82e0464 --- /dev/null +++ b/packages/boot/plugin-manager/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Plug-in-Verwaltungsdienst", + "description": "Verwalten Sie Plug-Ins und Plug-In-Pakete in der aktuell ausgeführten Konfiguration und unterstützen Sie Installation, Entfernung, Starten und Stoppen." + } +} diff --git a/packages/boot/plugin-manager/locale/es.json b/packages/boot/plugin-manager/locale/es.json new file mode 100644 index 0000000000..b4a6c2409a --- /dev/null +++ b/packages/boot/plugin-manager/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Servicio de gestión de complementos", + "description": "Administre complementos y paquetes de complementos en la configuración en ejecución actual, admitiendo la instalación, eliminación, inicio y detención." + } +} diff --git a/packages/boot/plugin-manager/locale/fr.json b/packages/boot/plugin-manager/locale/fr.json new file mode 100644 index 0000000000..51fb19f402 --- /dev/null +++ b/packages/boot/plugin-manager/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Service de gestion des plug-ins", + "description": "Gérez les plug-ins et les packages de plug-ins dans la configuration en cours d'exécution, prenant en charge l'installation, la suppression, le démarrage et l'arrêt." + } +} diff --git a/packages/boot/plugin-manager/locale/pt.json b/packages/boot/plugin-manager/locale/pt.json new file mode 100644 index 0000000000..63e8e2ffdb --- /dev/null +++ b/packages/boot/plugin-manager/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Serviço de gerenciamento de plug-ins", + "description": "Gerencie plug-ins e pacotes de plug-ins na configuração atual em execução, oferecendo suporte à instalação, remoção, início e parada." + } +} diff --git a/packages/boot/plugin-manager/locale/ru.json b/packages/boot/plugin-manager/locale/ru.json new file mode 100644 index 0000000000..1cd9a4db0d --- /dev/null +++ b/packages/boot/plugin-manager/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Служба управления плагинами", + "description": "Управляйте плагинами и пакетами плагинов в текущей работающей конфигурации, поддерживая установку, удаление, запуск и остановку." + } +} diff --git a/packages/boot/plugin-manager/locale/tools/de.json b/packages/boot/plugin-manager/locale/tools/de.json new file mode 100644 index 0000000000..0c51343ea1 --- /dev/null +++ b/packages/boot/plugin-manager/locale/tools/de.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Werkzeuge zur Plugin-Verwaltung" + } +} diff --git a/packages/boot/plugin-manager/locale/tools/es.json b/packages/boot/plugin-manager/locale/tools/es.json new file mode 100644 index 0000000000..43198339a7 --- /dev/null +++ b/packages/boot/plugin-manager/locale/tools/es.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Herramientas de gestión de plugins" + } +} diff --git a/packages/boot/plugin-manager/locale/tools/fr.json b/packages/boot/plugin-manager/locale/tools/fr.json new file mode 100644 index 0000000000..4119d2e33a --- /dev/null +++ b/packages/boot/plugin-manager/locale/tools/fr.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Outils de gestion des plugins" + } +} diff --git a/packages/boot/plugin-manager/locale/tools/pt.json b/packages/boot/plugin-manager/locale/tools/pt.json new file mode 100644 index 0000000000..d1b8e8d2ac --- /dev/null +++ b/packages/boot/plugin-manager/locale/tools/pt.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Ferramentas de gerenciamento de plug-ins" + } +} diff --git a/packages/boot/plugin-manager/locale/tools/ru.json b/packages/boot/plugin-manager/locale/tools/ru.json new file mode 100644 index 0000000000..9bf83c6a3d --- /dev/null +++ b/packages/boot/plugin-manager/locale/tools/ru.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Инструменты управления плагинами" + } +} diff --git a/packages/boot/plugin-market/README.i18n.yaml b/packages/boot/plugin-market/README.i18n.yaml index a9b46ad8cd..fb47f93c88 100644 --- a/packages/boot/plugin-market/README.i18n.yaml +++ b/packages/boot/plugin-market/README.i18n.yaml @@ -3,20 +3,20 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/boot/plugin-market/README.md /: - en: 847c61ab6dac5fd2 - zh: 4107ba58fbda723e + en: 043de7aa04105a53 + zh: b3d966c6029037e1 /deepseek-ai-dsh-plugin-market: en: ee5478f5717295df zh: 75a03a6db791fc6b /deepseek-ai-dsh-plugin-market/summary: - en: 5fc3f5439344cb9a - zh: 73e549c0b21b3843 + en: 4def97be3ad32ece + zh: f855102a82dbb6ec /deepseek-ai-dsh-plugin-market/table-of-contents: en: 76368b6b1da79c80 zh: 52384aee991ac542 /deepseek-ai-dsh-plugin-market/use-this-package: - en: 0553ab42ee4d83ff - zh: 4cc8163884357247 + en: fef9850d643e327f + zh: c6e8cab773cf93b9 /deepseek-ai-dsh-plugin-market/use-this-package/plugin-detail-documents: en: 0e15de273c9acaae zh: 4fd93c92b71d4398 @@ -39,8 +39,8 @@ en: 02cf6033092eb236 zh: cb928bfa6d39a7f7 /deepseek-ai-dsh-plugin-market/known-limitations-and-deferred-work: - en: 427d9c58ee09bea4 - zh: ec1c2bade3b56014 + en: 54122c96969ebbc2 + zh: 3fbe84bc6069cb46 /deepseek-ai-dsh-plugin-market/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/boot/plugin-market/README.md b/packages/boot/plugin-market/README.md index e805dcb2e4..b93adc2bf9 100644 --- a/packages/boot/plugin-market/README.md +++ b/packages/boot/plugin-market/README.md @@ -1,5 +1,5 @@ --- -description: "Host reader of the plugin market indexes a user configures, with installation status, compatibility, and the read-only plugin_market tool." +description: "Host reader of the plugin indexes a user configures, with installation status, compatibility, and the read-only plugin_market tool." kind: "package-reference" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -`pluginMarket` reads the plugin market indexes the user configures, validates them with [`dsh-plugin-market-index`](../../util/plugin-market-index/README.md), and annotates every entry with the profile's installation status, DSH compatibility, and the exact arguments that install it. It also reads each market entry's long description, a Markdown document with images, for Discover entry pages. The Web [Discover tab](../../client/ui-plugin-market/README.md) and the `plugin_market` agent tool read through it; installation itself goes through [`pluginManager.installBundle`](../plugin-manager/README.md). The format and its rationale live in the [index format Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.md); the Host and Web design lives in the [Discover Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.md). +`pluginMarket` reads the plugin indexes the user configures, validates them with [`dsh-plugin-market-index`](../../util/plugin-market-index/README.md), and annotates every entry with the profile's installation status, DSH compatibility, and the exact arguments that install it. It also reads each index entry's long description, a Markdown document with images, for Discover entry pages. The Web [Discover tab](../../client/ui-plugin-market/README.md) and the `plugin_market` agent tool read through it; installation itself goes through [`pluginManager.installBundle`](../plugin-manager/README.md). The format and its rationale live in the [index format Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.md); the Host and Web design lives in the [Discover Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.md). ## Table of Contents @@ -24,7 +24,7 @@ English | [中文](README.zh.md) ## Use this package -The optional [custom plugin markets bundle](../../experimental/plugin-market-bundle/README.md) mounts the service as the `plugin-market` row; shipped profiles leave the bundle off. Like `plugin-manager`, the row is disabled without a profile, and it waits for `pluginManager`. No index source is configured by default, so the Host sends no market request until a user adds one. +The optional [custom plugin indexes bundle](../../experimental/plugin-market-bundle/README.md) mounts the service as the `plugin-market` row; shipped profiles leave the bundle off. Like `plugin-manager`, the row is disabled without a profile, and it waits for `pluginManager`. No index source is configured by default, so the Host sends no index request until a user adds one. | Config | Default | Meaning | |---|---|---| @@ -123,10 +123,10 @@ Append-only: results enter the conversation as ordinary tool results and do not - **No upgrade** — an installed bundle with a newer index version is reported as `update-available`; upgrading remains uninstall followed by install. -- **Unsigned indexes** — index content, including a Git entry's name and version, is the market's claim; the installed manifest decides what is installed. +- **Unsigned indexes** — index content, including a Git entry's name and version, is the index's claim; the installed manifest decides what is installed. - **Last write wins** — `setIndexes` replaces the whole list, so two windows editing sources at the same time can overwrite each other's change. - **Redirects refused** — an index served only behind a redirect must be configured at its final URL. -- **Whole-document reads** — the Host downloads and keeps each index up to `maxBytes` and builds its text index in memory; a synthetic index of 20,000 plugins took about 0.8 s of build time and 130 MiB of heap. Parsing runs in a Worker, but receiving the validated index still blocks the Host event loop while the clone is deserialized: about 35 ms for a synthetic 7.6 MiB index of 20,000 plugins and about 190 ms for 38 MiB and 100,000 plugins. Larger markets need a server-side query protocol. +- **Whole-document reads** — the Host downloads and keeps each index up to `maxBytes` and builds its text index in memory; a synthetic index of 20,000 plugins took about 0.8 s of build time and 130 MiB of heap. Parsing runs in a Worker, but receiving the validated index still blocks the Host event loop while the clone is deserialized: about 35 ms for a synthetic 7.6 MiB index of 20,000 plugins and about 190 ms for 38 MiB and 100,000 plugins. Larger indexes need a server-side query protocol. - **Default-branch READMEs** — a repository README is read at the default branch, which can differ from the listed version; npm records no commit the CDNs expose. - **Private npm registries** — the public CDNs do not serve packages of other registries, so only the entry's repository can provide such a version's document. - **Fetching an unadvertised commit** — a pinned Git entry is fetched by commit id, which the server must allow (`uploadpack.allowReachableSHA1InWant` or `allowAnySHA1InWant`); GitHub and GitLab allow it, and a server that refuses reports `denied`. diff --git a/packages/boot/plugin-market/README.zh.md b/packages/boot/plugin-market/README.zh.md index 31dae179db..5d82b46c77 100644 --- a/packages/boot/plugin-market/README.zh.md +++ b/packages/boot/plugin-market/README.zh.md @@ -1,5 +1,5 @@ --- -description: "读取用户配置的插件市场索引的 Host 服务,提供安装状态、兼容性以及只读的 plugin_market 工具。" +description: "读取用户配置的插件索引的 Host 服务,提供安装状态、兼容性以及只读的 plugin_market 工具。" kind: "package-reference" --- @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -`pluginMarket` 读取用户配置的插件市场索引,用 [`dsh-plugin-market-index`](../../util/plugin-market-index/README.zh.md) 校验,并为每个条目标注 profile 中的安装状态、DSH 兼容性以及安装它所需的精确参数。它还为发现区的条目页面读取每个市场条目的详细介绍,即一份带图片的 Markdown 文档。Web 的[发现标签页](../../client/ui-plugin-market/README.zh.md)和 `plugin_market` agent 工具通过它读取;安装本身经由 [`pluginManager.installBundle`](../plugin-manager/README.zh.md) 进行。格式及其理由见[索引格式 Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.zh.md);Host 与 Web 的设计见[发现 Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.zh.md)。 +`pluginMarket` 读取用户配置的插件索引,用 [`dsh-plugin-market-index`](../../util/plugin-market-index/README.zh.md) 校验,并为每个条目标注 profile 中的安装状态、DSH 兼容性以及安装它所需的精确参数。它还为发现区的条目页面读取每个索引条目的详细介绍,即一份带图片的 Markdown 文档。Web 的[发现标签页](../../client/ui-plugin-market/README.zh.md)和 `plugin_market` agent 工具通过它读取;安装本身经由 [`pluginManager.installBundle`](../plugin-manager/README.zh.md) 进行。格式及其理由见[索引格式 Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.zh.md);Host 与 Web 的设计见[发现 Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-discover.zh.md)。 ## 目录 @@ -24,7 +24,7 @@ kind: "package-reference" ## 使用本包 -可选的[自定义插件市场组合包](../../experimental/plugin-market-bundle/README.zh.md)把该服务挂载为 `plugin-market` 行;随附的 profile 默认不启用该组合包。与 `plugin-manager` 一样,该行在没有 profile 时被禁用,并等待 `pluginManager`。默认不配置任何索引源,因此在用户添加索引源之前,Host 不发送任何市场请求。 +可选的[自定义插件索引组合包](../../experimental/plugin-market-bundle/README.zh.md)把该服务挂载为 `plugin-market` 行;随附的 profile 默认不启用该组合包。与 `plugin-manager` 一样,该行在没有 profile 时被禁用,并等待 `pluginManager`。默认不配置任何索引源,因此在用户添加索引源之前,Host 不发送任何索引请求。 | 配置 | 默认值 | 含义 | |---|---|---| @@ -124,10 +124,10 @@ Host 使用进程的 `fetch` 获取,因此配置的出站代理生效,并拒 - **不支持升级**——索引版本更新的已安装组合包报告为 `update-available`;升级仍需先卸载再安装。 -- **索引没有签名**——索引内容(包括 Git 条目的名称和版本)是市场的声明;实际安装什么由已安装的 manifest(元数据清单)决定。 +- **索引没有签名**——索引内容(包括 Git 条目的名称和版本)是索引的声明;实际安装什么由已安装的 manifest(元数据清单)决定。 - **后写入者覆盖**——`setIndexes` 会替换整个列表,因此两个窗口同时编辑索引源时,可能互相覆盖对方的修改。 - **拒绝重定向**——只能经重定向访问的索引必须以其最终 URL 配置。 -- **读取整份文档**——Host 下载并保留每个不超过 `maxBytes` 的索引,并在内存中构建其文本索引;一个包含 20,000 个插件的合成索引构建用时约 0.8 秒,占用约 130 MiB 堆内存。解析在 Worker 中运行,但接收校验后的索引时,反序列化 clone 仍会阻塞 Host 事件循环:包含 20,000 个插件的 7.6 MiB 合成索引约 35 毫秒,包含 100,000 个插件的 38 MiB 索引约 190 毫秒。更大的市场需要服务端查询协议。 +- **读取整份文档**——Host 下载并保留每个不超过 `maxBytes` 的索引,并在内存中构建其文本索引;一个包含 20,000 个插件的合成索引构建用时约 0.8 秒,占用约 130 MiB 堆内存。解析在 Worker 中运行,但接收校验后的索引时,反序列化 clone 仍会阻塞 Host 事件循环:包含 20,000 个插件的 7.6 MiB 合成索引约 35 毫秒,包含 100,000 个插件的 38 MiB 索引约 190 毫秒。更大的索引需要服务端查询协议。 - **默认分支的 README**——仓库 README 读取的是默认分支上的文件,可能与列出的版本不一致;CDN 公开的 npm 数据不记录提交。 - **私有 npm 注册表**——公共 CDN 不提供其他注册表的包,因此此类版本的文档只能来自条目的仓库。 - **获取未公布的提交**——固定版本的 Git 条目按提交 id 获取,这需要服务器允许(`uploadpack.allowReachableSHA1InWant` 或 `allowAnySHA1InWant`);GitHub 和 GitLab 允许这样做,拒绝的服务器报告 `denied`。 diff --git a/packages/boot/plugin-market/locale/de.json b/packages/boot/plugin-market/locale/de.json new file mode 100644 index 0000000000..68800a3ea4 --- /dev/null +++ b/packages/boot/plugin-market/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Plugin-Indizes", + "description": "Konfigurierte Plugin-Indizes für die Plugin-Suche und das Tool plugin_market verwalten." + } +} diff --git a/packages/boot/plugin-market/locale/en.json b/packages/boot/plugin-market/locale/en.json index 5baedc3f0f..8e479c69e9 100644 --- a/packages/boot/plugin-market/locale/en.json +++ b/packages/boot/plugin-market/locale/en.json @@ -1,6 +1,6 @@ { "meta": { - "title": "Plugin market indexes", - "description": "Manage configured market indexes for plugin discovery and the plugin_market tool." + "title": "Plugin indexes", + "description": "Manage configured plugin indexes for plugin discovery and the plugin_market tool." } } diff --git a/packages/boot/plugin-market/locale/es.json b/packages/boot/plugin-market/locale/es.json new file mode 100644 index 0000000000..bb6b779704 --- /dev/null +++ b/packages/boot/plugin-market/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Índices de complementos", + "description": "Administre los índices de complementos configurados para descubrir complementos y para la herramienta plugin_market." + } +} diff --git a/packages/boot/plugin-market/locale/fr.json b/packages/boot/plugin-market/locale/fr.json new file mode 100644 index 0000000000..9753ef1d48 --- /dev/null +++ b/packages/boot/plugin-market/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Index de plugins", + "description": "Gérez les index de plugins configurés pour la découverte de plugins et l’outil plugin_market." + } +} diff --git a/packages/boot/plugin-market/locale/ja.json b/packages/boot/plugin-market/locale/ja.json index 150534a797..0da10adfa0 100644 --- a/packages/boot/plugin-market/locale/ja.json +++ b/packages/boot/plugin-market/locale/ja.json @@ -1,6 +1,6 @@ { "meta": { - "title": "プラグインマーケットのインデックス", - "description": "プラグイン検索ページと plugin_market ツールで使う、設定済みのマーケットのインデックスを管理します。" + "title": "プラグインインデックス", + "description": "プラグイン検索ページと plugin_market ツールで使う、設定済みのプラグインインデックスを管理します。" } } diff --git a/packages/boot/plugin-market/locale/pt.json b/packages/boot/plugin-market/locale/pt.json new file mode 100644 index 0000000000..848d35af30 --- /dev/null +++ b/packages/boot/plugin-market/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Índices de plug-ins", + "description": "Gerencie os índices de plug-ins configurados para a descoberta de plug-ins e a ferramenta plugin_market." + } +} diff --git a/packages/boot/plugin-market/locale/ru.json b/packages/boot/plugin-market/locale/ru.json new file mode 100644 index 0000000000..8d80e808eb --- /dev/null +++ b/packages/boot/plugin-market/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Индексы плагинов", + "description": "Управляйте настроенными индексами плагинов для поиска плагинов и инструмента plugin_market." + } +} diff --git a/packages/boot/plugin-market/locale/zh.json b/packages/boot/plugin-market/locale/zh.json index 5113c07601..67a8526b9d 100644 --- a/packages/boot/plugin-market/locale/zh.json +++ b/packages/boot/plugin-market/locale/zh.json @@ -1,6 +1,6 @@ { "meta": { - "title": "插件市场索引", - "description": "为插件发现页面和 plugin_market 工具管理已配置的市场索引。" + "title": "插件索引", + "description": "为插件发现页面和 plugin_market 工具管理已配置的插件索引。" } } diff --git a/packages/boot/plugin-market/src/tools.ts b/packages/boot/plugin-market/src/tools.ts index f1783a2969..a881f45b9d 100644 --- a/packages/boot/plugin-market/src/tools.ts +++ b/packages/boot/plugin-market/src/tools.ts @@ -60,7 +60,7 @@ function paged(items: readonly T[], page: Page): { readonly slice: T[]; reado /** * Register the read-only `plugin_market` tool while the `pluginMarket` service is mounted. The service comes from the - * optional custom plugin markets bundle, so a preset can keep this row without the tool appearing or the row waiting. + * optional custom plugin indexes bundle, so a preset can keep this row without the tool appearing or the row waiting. * @param ctx - Agent-scoped tool registration context. */ export function apply(ctx: Context): void { @@ -136,6 +136,6 @@ function registerTool(ctx: Context): void { default: return assertNever(args.action) } }, - presentCall: args => ({ card: 'generic', title: 'Browse plugin market', kind: 'read', rawInput: args }), + presentCall: args => ({ card: 'generic', title: 'Browse plugin indexes', kind: 'read', rawInput: args }), })) } diff --git a/packages/browser-use/browser-use/locale/de.json b/packages/browser-use/browser-use/locale/de.json new file mode 100644 index 0000000000..21cf74b920 --- /dev/null +++ b/packages/browser-use/browser-use/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Registrierung des Browser-Anbieters", + "description": "Registrieren Sie Browser-Automatisierungsanbieter namentlich, mit einem Anbieter pro Name." + } +} diff --git a/packages/browser-use/browser-use/locale/es.json b/packages/browser-use/browser-use/locale/es.json new file mode 100644 index 0000000000..971b6dc650 --- /dev/null +++ b/packages/browser-use/browser-use/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Registro de proveedores de navegador", + "description": "Registre proveedores de automatización del navegador por nombre, con un proveedor por nombre." + } +} diff --git a/packages/browser-use/browser-use/locale/fr.json b/packages/browser-use/browser-use/locale/fr.json new file mode 100644 index 0000000000..a8d05dae20 --- /dev/null +++ b/packages/browser-use/browser-use/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Registre des fournisseurs de navigateur", + "description": "Enregistrez les fournisseurs d'automatisation de navigateur par nom, avec un fournisseur par nom." + } +} diff --git a/packages/browser-use/browser-use/locale/pt.json b/packages/browser-use/browser-use/locale/pt.json new file mode 100644 index 0000000000..a737a4da16 --- /dev/null +++ b/packages/browser-use/browser-use/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Registro do provedor de navegador", + "description": "Registre provedores de automação de navegador por nome, com um provedor por nome." + } +} diff --git a/packages/browser-use/browser-use/locale/ru.json b/packages/browser-use/browser-use/locale/ru.json new file mode 100644 index 0000000000..070de4c045 --- /dev/null +++ b/packages/browser-use/browser-use/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Реестр поставщиков браузеров", + "description": "Зарегистрируйте поставщиков автоматизации браузера по имени, по одному поставщику на имя." + } +} diff --git a/packages/bundle/acp-app/locale/de.json b/packages/bundle/acp-app/locale/de.json new file mode 100644 index 0000000000..69374bc3d2 --- /dev/null +++ b/packages/bundle/acp-app/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ACP-Profil", + "description": "Bietet rein automatisierte ACP-Kommunikation und Prozesslebenszyklusmanagement über die dsh-Basis." + } +} diff --git a/packages/bundle/acp-app/locale/es.json b/packages/bundle/acp-app/locale/es.json new file mode 100644 index 0000000000..3ea80933ba --- /dev/null +++ b/packages/bundle/acp-app/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Perfil ACP", + "description": "Proporcione comunicación ACP exclusivamente automatizada y gestión del ciclo de vida del proceso a través de la base dsh." + } +} diff --git a/packages/bundle/acp-app/locale/fr.json b/packages/bundle/acp-app/locale/fr.json new file mode 100644 index 0000000000..320c4228e2 --- /dev/null +++ b/packages/bundle/acp-app/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Profil ACP", + "description": "Fournissez une communication ACP uniquement automatisée et une gestion du cycle de vie des processus sur la base dsh." + } +} diff --git a/packages/bundle/acp-app/locale/pt.json b/packages/bundle/acp-app/locale/pt.json new file mode 100644 index 0000000000..b402f4d287 --- /dev/null +++ b/packages/bundle/acp-app/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ACP perfil", + "description": "Forneça comunicação ACP somente para automação e gerenciamento do ciclo de vida do processo em dshbase." + } +} diff --git a/packages/bundle/acp-app/locale/ru.json b/packages/bundle/acp-app/locale/ru.json new file mode 100644 index 0000000000..ef78a7910e --- /dev/null +++ b/packages/bundle/acp-app/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ACP профиль", + "description": "Обеспечьте связь только с ACP для автоматизации и управление жизненным циклом процессов через базу dsh." + } +} diff --git a/packages/bundle/headless/locale/de.json b/packages/bundle/headless/locale/de.json new file mode 100644 index 0000000000..df096072aa --- /dev/null +++ b/packages/bundle/headless/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Kopfloses Profil", + "description": "Führen Sie eine einzelne Aufgabe direkt mit dem Kernagenten/Sitzungsläufer über die dsh-Basis aus, ohne Host, HTTP oder Browserebene." + } +} diff --git a/packages/bundle/headless/locale/es.json b/packages/bundle/headless/locale/es.json new file mode 100644 index 0000000000..3916ac6658 --- /dev/null +++ b/packages/bundle/headless/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "perfil sin cabeza", + "description": "Ejecute una sola tarea directamente con el agente principal/ejecutor de sesión sobre la base dsh, sin un host, HTTP o una capa de navegador." + } +} diff --git a/packages/bundle/headless/locale/fr.json b/packages/bundle/headless/locale/fr.json new file mode 100644 index 0000000000..101db58c1b --- /dev/null +++ b/packages/bundle/headless/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Profil sans tête", + "description": "Exécutez une seule tâche directement avec le principal agent/exécuteur de session sur la base dsh, sans hôte, HTTP ou couche de navigateur." + } +} diff --git a/packages/bundle/headless/locale/pt.json b/packages/bundle/headless/locale/pt.json new file mode 100644 index 0000000000..ff379c6db0 --- /dev/null +++ b/packages/bundle/headless/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Perfil sem cabeça", + "description": "Execute uma única tarefa diretamente com o executor principal de agente/sessão em dsh-base, sem um host, HTTP ou camada de navegador." + } +} diff --git a/packages/bundle/headless/locale/ru.json b/packages/bundle/headless/locale/ru.json new file mode 100644 index 0000000000..e84fbba427 --- /dev/null +++ b/packages/bundle/headless/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Безголовый профиль", + "description": "Запустите одну задачу напрямую с помощью основного средства запуска агента/сеанса через базу dsh, без уровня хоста, HTTP или браузера." + } +} diff --git a/packages/bundle/sdk-app/locale/de.json b/packages/bundle/sdk-app/locale/de.json new file mode 100644 index 0000000000..efe87657eb --- /dev/null +++ b/packages/bundle/sdk-app/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "SDK-Profil", + "description": "Stellen Sie JSON-RPC über die Standardeingabe und -ausgabe bereit und verwalten Sie den Prozesslebenszyklus über die dsh-Basis." + } +} diff --git a/packages/bundle/sdk-app/locale/es.json b/packages/bundle/sdk-app/locale/es.json new file mode 100644 index 0000000000..b1b409e2b9 --- /dev/null +++ b/packages/bundle/sdk-app/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Perfil SDK", + "description": "Proporcione JSON-RPC a través de entrada y salida estándar y administre el ciclo de vida del proceso a través de la base dsh." + } +} diff --git a/packages/bundle/sdk-app/locale/fr.json b/packages/bundle/sdk-app/locale/fr.json new file mode 100644 index 0000000000..9de8913e48 --- /dev/null +++ b/packages/bundle/sdk-app/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Profil SDK", + "description": "Fournissez JSON-RPC sur une entrée et une sortie standard et gérez le cycle de vie du processus sur la base dsh." + } +} diff --git a/packages/bundle/sdk-app/locale/pt.json b/packages/bundle/sdk-app/locale/pt.json new file mode 100644 index 0000000000..187ca1d1e0 --- /dev/null +++ b/packages/bundle/sdk-app/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "SDK perfil", + "description": "Forneça JSON-RPC sobre entrada e saída padrão e gerencie o ciclo de vida do processo em dsh-base." + } +} diff --git a/packages/bundle/sdk-app/locale/ru.json b/packages/bundle/sdk-app/locale/ru.json new file mode 100644 index 0000000000..0502de6690 --- /dev/null +++ b/packages/bundle/sdk-app/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "SDK профиль", + "description": "Обеспечьте JSON-RPC поверх стандартного ввода и вывода и управляйте жизненным циклом процесса через dsh-base." + } +} diff --git a/packages/bundle/web-app/locale/de.json b/packages/bundle/web-app/locale/de.json new file mode 100644 index 0000000000..e7f067a8fd --- /dev/null +++ b/packages/bundle/web-app/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Unterstützung bei der Ausführung von Webanwendungen", + "description": "Stellen Sie Front-End-Seitenressourcen bereit und verbinden Sie die für die Webanwendung erforderliche laufende Konfiguration." + } +} diff --git a/packages/bundle/web-app/locale/es.json b/packages/bundle/web-app/locale/es.json new file mode 100644 index 0000000000..70e2cd3b9b --- /dev/null +++ b/packages/bundle/web-app/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Soporte de ejecución de aplicaciones web", + "description": "Proporcione recursos de la página de inicio y conecte la configuración en ejecución requerida por la aplicación web." + } +} diff --git a/packages/bundle/web-app/locale/fr.json b/packages/bundle/web-app/locale/fr.json new file mode 100644 index 0000000000..d33d200823 --- /dev/null +++ b/packages/bundle/web-app/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Prise en charge de l'exécution d'applications Web", + "description": "Fournissez les ressources de la page frontale et connectez la configuration en cours requise par l'application Web." + } +} diff --git a/packages/bundle/web-app/locale/pt.json b/packages/bundle/web-app/locale/pt.json new file mode 100644 index 0000000000..acf7b54d8e --- /dev/null +++ b/packages/bundle/web-app/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Suporte à execução de aplicações web", + "description": "Fornece recursos de página front-end e conecta a configuração de execução exigida pelo aplicativo da web." + } +} diff --git a/packages/bundle/web-app/locale/ru.json b/packages/bundle/web-app/locale/ru.json new file mode 100644 index 0000000000..bfddc6aa87 --- /dev/null +++ b/packages/bundle/web-app/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Поддержка выполнения веб-приложений", + "description": "Предоставьте ресурсы интерфейсной страницы и подключите работающую конфигурацию, необходимую веб-приложению." + } +} diff --git a/packages/bundle/web-app/locale/startup/de.json b/packages/bundle/web-app/locale/startup/de.json new file mode 100644 index 0000000000..3efc2eb5ef --- /dev/null +++ b/packages/bundle/web-app/locale/startup/de.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Web-Startkonfiguration" + } +} diff --git a/packages/bundle/web-app/locale/startup/es.json b/packages/bundle/web-app/locale/startup/es.json new file mode 100644 index 0000000000..b9ad99c565 --- /dev/null +++ b/packages/bundle/web-app/locale/startup/es.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Configuración de inicio web" + } +} diff --git a/packages/bundle/web-app/locale/startup/fr.json b/packages/bundle/web-app/locale/startup/fr.json new file mode 100644 index 0000000000..da69faa205 --- /dev/null +++ b/packages/bundle/web-app/locale/startup/fr.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Configuration du démarrage Web" + } +} diff --git a/packages/bundle/web-app/locale/startup/pt.json b/packages/bundle/web-app/locale/startup/pt.json new file mode 100644 index 0000000000..fe1f8baa0c --- /dev/null +++ b/packages/bundle/web-app/locale/startup/pt.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Configuração de inicialização Web" + } +} diff --git a/packages/bundle/web-app/locale/startup/ru.json b/packages/bundle/web-app/locale/startup/ru.json new file mode 100644 index 0000000000..c2c9f6862c --- /dev/null +++ b/packages/bundle/web-app/locale/startup/ru.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Настройки запуска веб-приложения" + } +} diff --git a/packages/client/connection/locale/de.json b/packages/client/connection/locale/de.json new file mode 100644 index 0000000000..85da6331b4 --- /dev/null +++ b/packages/client/connection/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Verbindung", + "description": "Richten Sie authentifizierte Client-Kommunikation ein und verwalten Sie den Verbindungslebenszyklus." + } +} diff --git a/packages/client/connection/locale/es.json b/packages/client/connection/locale/es.json new file mode 100644 index 0000000000..fa2080ac53 --- /dev/null +++ b/packages/client/connection/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "conexión", + "description": "Establezca comunicaciones autenticadas con el cliente y administre el ciclo de vida de la conexión." + } +} diff --git a/packages/client/connection/locale/fr.json b/packages/client/connection/locale/fr.json new file mode 100644 index 0000000000..d1665120c9 --- /dev/null +++ b/packages/client/connection/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "connexion", + "description": "Établissez des communications client authentifiées et gérez le cycle de vie des connexions." + } +} diff --git a/packages/client/connection/locale/pt.json b/packages/client/connection/locale/pt.json new file mode 100644 index 0000000000..760379ae67 --- /dev/null +++ b/packages/client/connection/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "conexão", + "description": "Estabeleça comunicações autenticadas de clientes e gerencie o ciclo de vida da conexão." + } +} diff --git a/packages/client/connection/locale/ru.json b/packages/client/connection/locale/ru.json new file mode 100644 index 0000000000..adfddadc3d --- /dev/null +++ b/packages/client/connection/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "соединение", + "description": "Установите аутентифицированную клиентскую связь и управляйте жизненным циклом соединения." + } +} diff --git a/packages/client/file-upload/locale/de.json b/packages/client/file-upload/locale/de.json new file mode 100644 index 0000000000..80c9e5ab6f --- /dev/null +++ b/packages/client/file-upload/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Datei-Upload", + "description": "Laden Sie Dateien vom Browser in die aktuelle Sitzung hoch, um sie von Nachrichten und Tools zu verwenden." + } +} diff --git a/packages/client/file-upload/locale/es.json b/packages/client/file-upload/locale/es.json new file mode 100644 index 0000000000..45732e15c5 --- /dev/null +++ b/packages/client/file-upload/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Carga de archivos", + "description": "Cargue archivos desde el navegador a la sesión actual para utilizarlos en mensajes y herramientas." + } +} diff --git a/packages/client/file-upload/locale/fr.json b/packages/client/file-upload/locale/fr.json new file mode 100644 index 0000000000..a518389efe --- /dev/null +++ b/packages/client/file-upload/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Téléchargement de fichiers", + "description": "Téléchargez des fichiers du navigateur vers la session en cours pour les utiliser par les messages et les outils." + } +} diff --git a/packages/client/file-upload/locale/pt.json b/packages/client/file-upload/locale/pt.json new file mode 100644 index 0000000000..e50ef62390 --- /dev/null +++ b/packages/client/file-upload/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Carregamento de arquivo", + "description": "Carregue arquivos do navegador para a sessão atual para uso por mensagens e ferramentas." + } +} diff --git a/packages/client/file-upload/locale/ru.json b/packages/client/file-upload/locale/ru.json new file mode 100644 index 0000000000..b8a733cc10 --- /dev/null +++ b/packages/client/file-upload/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Загрузка файла", + "description": "Загружайте файлы из браузера в текущий сеанс для использования сообщениями и инструментами." + } +} diff --git a/packages/client/hmr/locale/de.json b/packages/client/hmr/locale/de.json new file mode 100644 index 0000000000..6a8ba35a12 --- /dev/null +++ b/packages/client/hmr/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Hmr", + "description": "Synchronisieren Sie die Plug-Ins im Browser und aktualisieren Sie die entsprechende Schnittstelle, nachdem der Code neu erstellt wurde." + } +} diff --git a/packages/client/hmr/locale/es.json b/packages/client/hmr/locale/es.json new file mode 100644 index 0000000000..f2ca5cf4fa --- /dev/null +++ b/packages/client/hmr/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "hmr", + "description": "Sincronice los complementos en el navegador y actualice la interfaz relevante después de reconstruir el código." + } +} diff --git a/packages/client/hmr/locale/fr.json b/packages/client/hmr/locale/fr.json new file mode 100644 index 0000000000..295a751727 --- /dev/null +++ b/packages/client/hmr/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "hum", + "description": "Synchronisez les plug-ins dans le navigateur et mettez à jour l'interface correspondante une fois le code reconstruit." + } +} diff --git a/packages/client/hmr/locale/pt.json b/packages/client/hmr/locale/pt.json new file mode 100644 index 0000000000..33c1516cf2 --- /dev/null +++ b/packages/client/hmr/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "hum", + "description": "Sincronize os plug-ins no navegador e atualize a interface relevante após a reconstrução do código." + } +} diff --git a/packages/client/hmr/locale/ru.json b/packages/client/hmr/locale/ru.json new file mode 100644 index 0000000000..cb8f218fe1 --- /dev/null +++ b/packages/client/hmr/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "хмр", + "description": "Синхронизируйте плагины в браузере и обновите соответствующий интерфейс после перестройки кода." + } +} diff --git a/packages/client/locale/README.i18n.yaml b/packages/client/locale/README.i18n.yaml index f2259ee13c..a90c3133e5 100644 --- a/packages/client/locale/README.i18n.yaml +++ b/packages/client/locale/README.i18n.yaml @@ -9,8 +9,8 @@ en: 9365090d78467dea zh: 74b6fbb9769d795f /deepseek-ai-dsh-client-locale/summary: - en: a2c282377f0a043e - zh: 2274f1ea41774549 + en: cbbe8f61f5e19f39 + zh: 2cec7ec41f1a7e84 /deepseek-ai-dsh-client-locale/table-of-contents: en: d152484eb41ac6b4 zh: bda879a892f240f0 @@ -21,8 +21,8 @@ en: 2b82458e185ba321 zh: e55a7e709a3f0158 /deepseek-ai-dsh-client-locale/use-this-package/registering-a-dictionary: - en: 69b07cde5b2b4deb - zh: b8204c127b10a711 + en: f19407c3ce8a94cc + zh: 3e5300ce7fa02987 /deepseek-ai-dsh-client-locale/use-this-package/resolving-package-text: en: 3b63e84fe0fdf8f4 zh: e97b5d1b92dff7ec @@ -45,7 +45,7 @@ en: 68373e745761494c zh: 43a5b8c19d03268a /deepseek-ai-dsh-client-locale/understand-the-implementation/source-map: - en: fc93a8f6804aa5bd + en: 6f329e4872398634 zh: 9d8364dc4c03d055 /deepseek-ai-dsh-client-locale/further-exploration: en: f73d411c9e90dead diff --git a/packages/client/locale/README.md b/packages/client/locale/README.md index e5afbfbde0..0ed329b47a 100644 --- a/packages/client/locale/README.md +++ b/packages/client/locale/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Use `dsh-client-locale` to switch the web GUI between the shipped English, Chinese, and Japanese locales or languages added by client plugins. User selections take effect immediately; every authenticated browser persists them as `locale.preference` in `$DSH_HOME/cordis.patch.yml` through the Host settings service. New browsers use the first supported language requested by the browser until the Host read delivers a stored preference. Plugin authors add typed namespace dictionaries and translate through the public locale API; slot-rendered copy updates without a reload. +Use `dsh-client-locale` to switch the web GUI between the shipped English, Chinese, Japanese, Spanish, French, German, Russian, and Brazilian Portuguese (`pt`, including `pt-PT` browser requests) locales or languages added by client plugins. User selections take effect immediately; every authenticated browser persists them as `locale.preference` in `$DSH_HOME/cordis.patch.yml` through the Host settings service. New browsers use the first supported language requested by the browser until the Host read delivers a stored preference. Plugin authors add typed namespace dictionaries and translate through the public locale API; slot-rendered copy updates without a reload. ## Table of Contents @@ -37,7 +37,7 @@ Native shells may provide `__DSH_LOCALE__` with an asynchronous `read()` and an ### Registering a dictionary -Call `ctx.locale.register(ns, { zh, en, ja })` with a namespace merged into `LocaleNamespaceMap`; the compiler checks every key against the namespace's typed key union and requires both baseline locales (`zh` and `en`). Additional locale ids are optional, and their dictionaries are checked against the same namespace keys. Consumers translate through `ctx.locale.bind(ns)` or the framework-injected `t` seat. A dictionary registered after the UI is already mounted is picked up without a remount. +Call `ctx.locale.register(ns, { zh, en, ja })` with a namespace merged into `LocaleNamespaceMap`; the compiler checks every key against the namespace's typed key union and requires both baseline locales (`zh` and `en`). Additional locale ids are optional, and their dictionaries are checked against the same namespace keys. Consumers translate through `ctx.locale.bind(ns)` or the framework-injected `t` seat. A dictionary registered after the UI is already mounted is picked up without a remount. Preserve named placeholders, slash commands, and API field names verbatim in translations; `pnpm run check:i18n` checks first-party dictionaries and rejects zero-width spaces (U+200B) and byte-order marks (U+FEFF) in locale resources. ### Resolving package text @@ -52,13 +52,13 @@ export const inject = ['locale'] export function apply(ctx) { ctx.effect( - () => ctx.locale.addLanguage({ id: 'fr', label: 'Français', fallback: 'en' }), + () => ctx.locale.addLanguage({ id: 'it', label: 'Italiano', fallback: 'en' }), 'my-locale: language', ) ctx.effect( - () => ctx.locale.register('common', 'fr', { - cancel: 'Annuler', - close: 'Fermer', + () => ctx.locale.register('common', 'it', { + cancel: 'Annulla', + close: 'Chiudi', }), 'my-locale: common dictionary', ) @@ -102,7 +102,7 @@ The typed object form requires complete dictionaries for both baseline locales. | [`src/client/index.ts`](src/client/index.ts) | `LocaleRuntime`, dictionary registry, Language row registration, `locale/change` event | | [`src/index.ts`](src/index.ts) | Node half: registers the `locale` settings namespace | | [`src/locale-settings.ts`](src/locale-settings.ts) | The durable schema for `locale.preference` | -| [`src/locales/`](src/locales/) | The shipped `zh`/`en`/`ja` dictionaries | +| [`src/locales/`](src/locales/) | The shipped language dictionaries | diff --git a/packages/client/locale/README.zh.md b/packages/client/locale/README.zh.md index 84965430c9..4c2d8037ed 100644 --- a/packages/client/locale/README.zh.md +++ b/packages/client/locale/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -使用 `dsh-client-locale` 可在 web GUI 中切换内置的英文、中文和日文 locale,或 client 插件添加的语言。用户选择会立即生效;每个已认证浏览器都通过 Host settings 服务把选择以 `locale.preference` 持久化到 `$DSH_HOME/cordis.patch.yml`。全新浏览器会使用浏览器请求的第一个受支持语言,直到 Host 读取送达已存储偏好。插件作者可添加类型化命名空间字典,并通过公开 locale API 翻译;经 slot 渲染的文案无需重新加载即可随语言切换更新。 +使用 `dsh-client-locale` 可在 web GUI 中切换内置的英文、中文、日语、西班牙语、法语、德语、俄语和巴西葡萄牙语(`pt`,也匹配 `pt-PT` 浏览器请求)locale,或 client 插件添加的语言。用户选择会立即生效;每个已认证浏览器都通过 Host settings 服务把选择以 `locale.preference` 持久化到 `$DSH_HOME/cordis.patch.yml`。全新浏览器会使用浏览器请求的第一个受支持语言,直到 Host 读取送达已存储偏好。插件作者可添加类型化命名空间字典,并通过公开 locale API 翻译;经 slot 渲染的文案无需重新加载即可随语言切换更新。 ## 目录 @@ -37,7 +37,7 @@ kind: "package-reference" ### 注册字典 -用已合并进 `LocaleNamespaceMap` 的命名空间调用 `ctx.locale.register(ns, { zh, en, ja })`;编译器会对照该命名空间的类型化键并集检查每个键,并要求两个基础 locale(`zh` 和 `en`)齐全。其他 locale id 为可选项,其字典同样按该命名空间的键检查。消费方通过 `ctx.locale.bind(ns)` 或框架注入的 `t` 席位翻译。UI 已挂载后再注册的字典无需重新挂载即可生效。 +用已合并进 `LocaleNamespaceMap` 的命名空间调用 `ctx.locale.register(ns, { zh, en, ja })`;编译器会对照该命名空间的类型化键并集检查每个键,并要求两个基础 locale(`zh` 和 `en`)齐全。其他 locale id 为可选项,其字典同样按该命名空间的键检查。消费方通过 `ctx.locale.bind(ns)` 或框架注入的 `t` 席位翻译。UI 已挂载后再注册的字典无需重新挂载即可生效。翻译必须原样保留命名占位符、斜杠命令和 API 字段名;`pnpm run check:i18n` 检查仓库内的字典,并拒绝语言资源中的零宽空格(U+200B)和字节顺序标记(U+FEFF)。 ### 解析包文本 @@ -52,13 +52,13 @@ export const inject = ['locale'] export function apply(ctx) { ctx.effect( - () => ctx.locale.addLanguage({ id: 'fr', label: 'Français', fallback: 'en' }), + () => ctx.locale.addLanguage({ id: 'it', label: 'Italiano', fallback: 'en' }), 'my-locale: language', ) ctx.effect( - () => ctx.locale.register('common', 'fr', { - cancel: 'Annuler', - close: 'Fermer', + () => ctx.locale.register('common', 'it', { + cancel: 'Annulla', + close: 'Chiudi', }), 'my-locale: common dictionary', ) diff --git a/packages/client/locale/locale/de.json b/packages/client/locale/locale/de.json new file mode 100644 index 0000000000..f7eb734545 --- /dev/null +++ b/packages/client/locale/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Gebietsschema", + "description": "Verwalten Sie die Spracheinstellungen der Benutzeroberfläche und die Übersetzungskopie jedes Plug-Ins." + } +} diff --git a/packages/client/locale/locale/es.json b/packages/client/locale/locale/es.json new file mode 100644 index 0000000000..ea4d2f0869 --- /dev/null +++ b/packages/client/locale/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "local", + "description": "Administre las preferencias de idioma de la interfaz y la copia de traducción de cada complemento." + } +} diff --git a/packages/client/locale/locale/fr.json b/packages/client/locale/locale/fr.json new file mode 100644 index 0000000000..c3ff597891 --- /dev/null +++ b/packages/client/locale/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "paramètres régionaux", + "description": "Gérez les préférences linguistiques de l’interface et la copie de traduction de chaque plug-in." + } +} diff --git a/packages/client/locale/locale/pt.json b/packages/client/locale/locale/pt.json new file mode 100644 index 0000000000..7b4b751941 --- /dev/null +++ b/packages/client/locale/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "localidade", + "description": "Gerencie as preferências de idioma da interface e a cópia de tradução de cada plug-in." + } +} diff --git a/packages/client/locale/locale/ru.json b/packages/client/locale/locale/ru.json new file mode 100644 index 0000000000..4b11903438 --- /dev/null +++ b/packages/client/locale/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "локаль", + "description": "Управляйте языковыми настройками интерфейса и копией перевода каждого плагина." + } +} diff --git a/packages/client/locale/src/client/index.ts b/packages/client/locale/src/client/index.ts index c6484b8908..3ba1bae29b 100644 --- a/packages/client/locale/src/client/index.ts +++ b/packages/client/locale/src/client/index.ts @@ -20,9 +20,10 @@ import { LOCALE_ID_PATTERN, LOCALE_IDS, LOCALE_PREFERENCE_FIELD, LOCALE_SETTINGS_NAMESPACE, type BuiltInLocaleId, type LocaleId, type LocaleSettings, } from '../locale-settings.ts' -import { en, ja, zh, type CommonKey } from '../locales/index.ts' +import { en, ja, es, fr, de, ru, pt, zh, type CommonKey } from '../locales/index.ts' import { - en as settingsEn, ja as settingsJa, zh as settingsZh, type SettingsLocaleKey, + en as settingsEn, ja as settingsJa, es as settingsEs, fr as settingsFr, + de as settingsDe, ru as settingsRu, pt as settingsPt, zh as settingsZh, type SettingsLocaleKey, } from '../locales/settings.ts' import type { LanguageRowInjected } from './LanguageRow.tsx' import { LanguageRow } from './LanguageRow.tsx' @@ -579,9 +580,11 @@ export async function apply(ctx: ClientContext): Promise { } const host = ctx.configForms.get(LOCALE_SETTINGS_NAMESPACE) const locale = new LocaleRuntime(ctx, host, bootstrap) - ctx.effect(() => locale.addLanguage({ id: 'ja', label: '日本語', fallback: 'en' }), 'locale: Japanese language') - ctx.effect(() => locale.register(COMMON_NS, { zh, en, ja }), 'locale: common dictionaries') - ctx.effect(() => locale.register(SETTINGS_NS, { zh: settingsZh, en: settingsEn, ja: settingsJa }), 'locale: settings dictionaries') + for (const [id, label] of [['ja', '日本語'], ['es', 'Español'], ['fr', 'Français'], ['de', 'Deutsch'], ['ru', 'Русский'], ['pt', 'Português']] as const) { + ctx.effect(() => locale.addLanguage({ id, label, fallback: 'en' }), `locale: ${id} language`) + } + ctx.effect(() => locale.register(COMMON_NS, { zh, en, ja, es, fr, de, ru, pt }), 'locale: common dictionaries') + ctx.effect(() => locale.register(SETTINGS_NS, { zh: settingsZh, en: settingsEn, ja: settingsJa, es: settingsEs, fr: settingsFr, de: settingsDe, ru: settingsRu, pt: settingsPt }), 'locale: settings dictionaries') ctx.provide('locale', locale) if (bridge !== undefined) { ctx.on('locale/change', (snapshot) => { bridge.onChange(snapshot.active) }) diff --git a/packages/client/locale/src/locales/de.ts b/packages/client/locale/src/locales/de.ts new file mode 100644 index 0000000000..dd88cf6fc4 --- /dev/null +++ b/packages/client/locale/src/locales/de.ts @@ -0,0 +1,47 @@ +/** German common-namespace dictionary. */ +import type { en } from './en.ts' + +/** Complete German copy for the common namespace. */ +export const de = { + 'ok': 'OK', + 'cancel': 'Abbrechen', + 'close': 'Schließen', + 'copy': 'Kopieren', + 'copied': 'Kopiert', + 'codeBlock.title': 'Codeblock', + 'codeBlock.wrap': 'Zeilen umbrechen', + 'codeBlock.unwrap': 'Zeilen nicht umbrechen', + 'copy.failed': 'Das Kopieren ist fehlgeschlagen', + 'copy.value': 'Wert kopieren', + 'copy.json': 'JSON kopieren', + 'copy.path': 'Eigenschaftspfad kopieren', + 'copy.prettyJson': 'Formatiertes JSON kopieren', + 'copy.compactJson': 'Kompaktes JSON kopieren', + 'copy.optionsHint': '{action}; Klicken Sie mit der rechten Maustaste, um die Kopieroptionen anzuzeigen', + 'retry': 'Erneut versuchen', + 'loading': 'Laden…', + 'load.failed': 'Laden fehlgeschlagen', + 'submit': 'Senden', + 'submitting': 'Wird gesendet…', + 'next': 'Weiter', + 'previous': 'Zurück', + 'skip': 'Überspringen', + 'delete': 'Löschen', + 'edit': 'Bearbeiten', + 'save': 'Speichern', + 'search': 'Suchen', + 'more': 'Mehr', + 'collapse': 'Einklappen', + 'expand': 'Ausklappen', + 'back': 'Zurück', + 'brand.localBuild': 'Lokaler DSH-Build', + 'workspace.defaultName': 'Standardarbeitsbereich', + 'unknown': 'Unbekannt', + 'none': 'Keine', + 'truncated': 'Abgeschnitten', + 'json.label': 'JSON', + 'markdown.footnotes': 'Fußnoten', + 'markdown.truncatedCharacters': '… nach {total} Zeichen abgeschnitten', + 'number.thousand': '{value}K', + 'number.million': '{value}M', +} satisfies Record diff --git a/packages/client/locale/src/locales/es.ts b/packages/client/locale/src/locales/es.ts new file mode 100644 index 0000000000..7eb95374e1 --- /dev/null +++ b/packages/client/locale/src/locales/es.ts @@ -0,0 +1,47 @@ +/** Spanish common-namespace dictionary. */ +import type { en } from './en.ts' + +/** Complete Spanish copy for the common namespace. */ +export const es = { + 'ok': 'OK', + 'cancel': 'Cancelar', + 'close': 'Cerrar', + 'copy': 'Copiar', + 'copied': 'Copiado', + 'codeBlock.title': 'Bloque de código', + 'codeBlock.wrap': 'Ajustar líneas', + 'codeBlock.unwrap': 'No ajustar líneas', + 'copy.failed': 'Copia fallida', + 'copy.value': 'Copiar valor', + 'copy.json': 'Copiar JSON', + 'copy.path': 'Copiar ruta de propiedad', + 'copy.prettyJson': 'Copiar JSON con formato', + 'copy.compactJson': 'Copiar JSON compacto', + 'copy.optionsHint': '{action}; haga clic derecho para opciones de copia', + 'retry': 'Reintentar', + 'loading': 'Cargando…', + 'load.failed': 'No se pudo cargar', + 'submit': 'Enviar', + 'submitting': 'Enviando…', + 'next': 'Siguiente', + 'previous': 'Anterior', + 'skip': 'Saltar', + 'delete': 'Eliminar', + 'edit': 'Editar', + 'save': 'Guardar', + 'search': 'Buscar', + 'more': 'Más', + 'collapse': 'Contraer', + 'expand': 'Expandir', + 'back': 'Atrás', + 'brand.localBuild': 'Compilación local de DSH', + 'workspace.defaultName': 'Espacio de trabajo predeterminado', + 'unknown': 'Desconocido', + 'none': 'Ninguno', + 'truncated': 'Truncado', + 'json.label': 'JSON', + 'markdown.footnotes': 'Notas a pie de página', + 'markdown.truncatedCharacters': '… truncado a {total} caracteres', + 'number.thousand': '{value}K', + 'number.million': '{value}M', +} satisfies Record diff --git a/packages/client/locale/src/locales/fr.ts b/packages/client/locale/src/locales/fr.ts new file mode 100644 index 0000000000..c57f1a3b11 --- /dev/null +++ b/packages/client/locale/src/locales/fr.ts @@ -0,0 +1,47 @@ +/** French common-namespace dictionary. */ +import type { en } from './en.ts' + +/** Complete French copy for the common namespace. */ +export const fr = { + 'ok': 'OK', + 'cancel': 'Annuler', + 'close': 'Fermer', + 'copy': 'Copier', + 'copied': 'Copié', + 'codeBlock.title': 'Bloc de code', + 'codeBlock.wrap': 'Activer le retour à la ligne', + 'codeBlock.unwrap': 'Désactiver le retour à la ligne', + 'copy.failed': 'Échec de la copie', + 'copy.value': 'Copier la valeur', + 'copy.json': 'Copier le JSON', + 'copy.path': 'Copier le chemin de la propriété', + 'copy.prettyJson': 'Copier le JSON formaté', + 'copy.compactJson': 'Copier le JSON compact', + 'copy.optionsHint': '{action}; clic droit pour les options de copie', + 'retry': 'Réessayer', + 'loading': 'Chargement…', + 'load.failed': 'Échec du chargement', + 'submit': 'Soumettre', + 'submitting': 'Envoi…', + 'next': 'Suivant', + 'previous': 'Précédent', + 'skip': 'Sauter', + 'delete': 'Supprimer', + 'edit': 'Modifier', + 'save': 'Enregistrer', + 'search': 'Rechercher', + 'more': 'Plus', + 'collapse': 'Réduire', + 'expand': 'Développer', + 'back': 'Retour', + 'brand.localBuild': 'Version locale de DSH', + 'workspace.defaultName': 'Espace de travail par défaut', + 'unknown': 'Inconnu', + 'none': 'Aucun', + 'truncated': 'Tronqué', + 'json.label': 'JSON', + 'markdown.footnotes': 'Notes de bas de page', + 'markdown.truncatedCharacters': '… tronqué à {total} caractères', + 'number.thousand': '{value}K', + 'number.million': '{value}M', +} satisfies Record diff --git a/packages/client/locale/src/locales/index.ts b/packages/client/locale/src/locales/index.ts index a01489ccfd..6690d74aeb 100644 --- a/packages/client/locale/src/locales/index.ts +++ b/packages/client/locale/src/locales/index.ts @@ -1,8 +1,13 @@ /** - * Common-namespace dictionaries. Chinese defines the keys; English and - * Japanese are checked against the same keys by TypeScript. + * Common-namespace dictionaries. Chinese defines the keys; + * every shipped language is checked against them by TypeScript. */ export { zh } from './zh.ts' export { en } from './en.ts' export { ja } from './ja.ts' export type { CommonKey } from './zh.ts' +export { es } from './es.ts' +export { fr } from './fr.ts' +export { de } from './de.ts' +export { ru } from './ru.ts' +export { pt } from './pt.ts' diff --git a/packages/client/locale/src/locales/pt.ts b/packages/client/locale/src/locales/pt.ts new file mode 100644 index 0000000000..75cdb19c29 --- /dev/null +++ b/packages/client/locale/src/locales/pt.ts @@ -0,0 +1,47 @@ +/** Portuguese common-namespace dictionary. */ +import type { en } from './en.ts' + +/** Complete Portuguese copy for the common namespace. */ +export const pt = { + 'ok': 'OK', + 'cancel': 'Cancelar', + 'close': 'Fechar', + 'copy': 'Copiar', + 'copied': 'Copiado', + 'codeBlock.title': 'Bloco de código', + 'codeBlock.wrap': 'Quebrar linhas', + 'codeBlock.unwrap': 'Não quebrar linhas', + 'copy.failed': 'Falha na cópia', + 'copy.value': 'Copiar valor', + 'copy.json': 'Copiar JSON', + 'copy.path': 'Copiar caminho da propriedade', + 'copy.prettyJson': 'Copiar JSON formatado', + 'copy.compactJson': 'Copiar JSON compacto', + 'copy.optionsHint': '{action}; clique com o botão direito para opções de cópia', + 'retry': 'Tentar novamente', + 'loading': 'Carregando…', + 'load.failed': 'Falha ao carregar', + 'submit': 'Enviar', + 'submitting': 'Enviando…', + 'next': 'Próximo', + 'previous': 'Anterior', + 'skip': 'Pular', + 'delete': 'Excluir', + 'edit': 'Editar', + 'save': 'Salvar', + 'search': 'Pesquisar', + 'more': 'Mais', + 'collapse': 'Recolher', + 'expand': 'Expandir', + 'back': 'Voltar', + 'brand.localBuild': 'Versão local do DSH', + 'workspace.defaultName': 'Espaço de trabalho padrão', + 'unknown': 'Desconhecido', + 'none': 'Nenhum', + 'truncated': 'Truncado', + 'json.label': 'JSON', + 'markdown.footnotes': 'Notas de rodapé', + 'markdown.truncatedCharacters': '… truncado em {total} caracteres', + 'number.thousand': '{value}K', + 'number.million': '{value}M', +} satisfies Record diff --git a/packages/client/locale/src/locales/ru.ts b/packages/client/locale/src/locales/ru.ts new file mode 100644 index 0000000000..a8a6d829da --- /dev/null +++ b/packages/client/locale/src/locales/ru.ts @@ -0,0 +1,47 @@ +/** Russian common-namespace dictionary. */ +import type { en } from './en.ts' + +/** Complete Russian copy for the common namespace. */ +export const ru = { + 'ok': 'OK', + 'cancel': 'Отмена', + 'close': 'Закрыть', + 'copy': 'Копировать', + 'copied': 'Скопировано', + 'codeBlock.title': 'Блок кода', + 'codeBlock.wrap': 'Переносить строки', + 'codeBlock.unwrap': 'Не переносить строки', + 'copy.failed': 'Не удалось скопировать', + 'copy.value': 'Копировать значение', + 'copy.json': 'Копировать JSON', + 'copy.path': 'Копировать путь к свойству', + 'copy.prettyJson': 'Копировать форматированный JSON', + 'copy.compactJson': 'Копировать компактный JSON', + 'copy.optionsHint': '{action}; щелкните правой кнопкой мыши, чтобы просмотреть параметры копирования', + 'retry': 'Повторить', + 'loading': 'Загрузка…', + 'load.failed': 'Не удалось загрузить', + 'submit': 'Отправить', + 'submitting': 'Отправка…', + 'next': 'Далее', + 'previous': 'Назад', + 'skip': 'Пропустить', + 'delete': 'Удалить', + 'edit': 'Редактировать', + 'save': 'Сохранить', + 'search': 'Поиск', + 'more': 'Ещё', + 'collapse': 'Свернуть', + 'expand': 'Развернуть', + 'back': 'Назад', + 'brand.localBuild': 'Локальная сборка DSH', + 'workspace.defaultName': 'Рабочая область по умолчанию', + 'unknown': 'Неизвестно', + 'none': 'Нет', + 'truncated': 'Усечено', + 'json.label': 'JSON', + 'markdown.footnotes': 'Сноски', + 'markdown.truncatedCharacters': '… обрезано до {total} символов', + 'number.thousand': '{value}K', + 'number.million': '{value}M', +} satisfies Record diff --git a/packages/client/locale/src/locales/settings.ts b/packages/client/locale/src/locales/settings.ts index 76c5fd1ffb..8aafb48c44 100644 --- a/packages/client/locale/src/locales/settings.ts +++ b/packages/client/locale/src/locales/settings.ts @@ -17,3 +17,28 @@ export const en = { export const ja = { 'language.title': '言語', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'language.title': 'Idioma', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'language.title': 'Langue', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'language.title': 'Sprache', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'language.title': 'Язык', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'language.title': 'Idioma', +} satisfies Record diff --git a/packages/client/locale/tests/apply.client.spec.ts b/packages/client/locale/tests/apply.client.spec.ts index 97209c4ab3..43920d60c0 100644 --- a/packages/client/locale/tests/apply.client.spec.ts +++ b/packages/client/locale/tests/apply.client.spec.ts @@ -1,3 +1,4 @@ +import { SHIPPED_LOCALE_IDS } from '../../../../scripts/shipped-locales.ts' /** locale apply wiring: service + dictionaries provision, declaration-aware * Language row registration, snapshot projection into the row store, and * recovery after an HMR collapse of the declaring entry. */ @@ -155,7 +156,7 @@ describe('locale apply', () => { const { entry, instance, face } = faceOf(b.slots) // The inject-time re-sync sealed the init window: the mirror is current. expect(instance.getSnapshot().active).toBe('en') - expect(instance.getSnapshot().options.map(o => o.id)).toEqual(['zh', 'en', 'ja']) + expect(instance.getSnapshot().options.map(o => o.id).sort()).toEqual([...SHIPPED_LOCALE_IDS].sort()) // Copy rides the standard locale seat: the entry declares the namespace. expect(entry.locale).toBe(SETTINGS_NS) expect(locale.bind(SETTINGS_NS)('language.title')).toBe('Language') @@ -176,7 +177,7 @@ describe('locale apply', () => { const languagePack = b.ctx.plugin({ inject: ['locale'], apply: packCtx => packCtx.effect( - () => packCtx.locale.addLanguage({ id: 'fr', label: 'Français', fallback: 'en' }), + () => packCtx.locale.addLanguage({ id: 'it', label: 'Italiano', fallback: 'en' }), 'test language pack registration', ), }) @@ -185,33 +186,45 @@ describe('locale apply', () => { { id: 'zh', label: '中文' }, { id: 'en', label: 'English' }, { id: 'ja', label: '日本語' }, + { id: 'es', label: 'Español' }, { id: 'fr', label: 'Français' }, + { id: 'de', label: 'Deutsch' }, + { id: 'ru', label: 'Русский' }, + { id: 'pt', label: 'Português' }, + { id: 'it', label: 'Italiano' }, ]) await languagePack.dispose() - expect(instance.getSnapshot().options.map(option => option.id)).toEqual(['zh', 'en', 'ja']) + expect(instance.getSnapshot().options.map(option => option.id)).toEqual(['zh', 'en', 'ja', 'es', 'fr', 'de', 'ru', 'pt']) }) - it('restores Japanese from the Host and releases its language and dictionaries on unload', async () => { - const b = await bench('ja') + it.each([ + ['ja', '日本語', '言語', 'キャンセル'], + ['es', 'Español', 'Idioma', 'Cancelar'], + ['fr', 'Français', 'Langue', 'Annuler'], + ['de', 'Deutsch', 'Sprache', 'Abbrechen'], + ['ru', 'Русский', 'Язык', 'Отмена'], + ['pt', 'Português', 'Idioma', 'Cancelar'], + ])('restores %s from the Host and releases its language and dictionaries on unload', async (id, label, languageTitle, cancel) => { + const b = await bench(id) onTestFinished(() => b.ctx.fiber.dispose()) declareItems(b.slots) const fiber = b.ctx.plugin({ inject: [...inject], apply }) await fiber.await() const locale = b.ctx.get('locale') as LocaleRuntime const { instance, face } = faceOf(b.slots) - expect(instance.getSnapshot().active).toBe('ja') - expect(locale.bind(SETTINGS_NS)('language.title')).toBe('言語') - expect(locale.bind('common')('cancel')).toBe('キャンセル') + expect(instance.getSnapshot().active).toBe(id) + expect(locale.bind(SETTINGS_NS)('language.title')).toBe(languageTitle) + expect(locale.bind('common')('cancel')).toBe(cancel) face.setLocale('en') - face.setLocale('ja') + face.setLocale(id) await vi.waitFor(() => { expect(b.mutate).toHaveBeenCalledTimes(2) }) - expect(b.mutate.mock.calls.at(-1)?.[1]).toEqual([{ op: 'set', path: ['preference'], value: 'ja' }]) + expect(b.mutate.mock.calls.at(-1)?.[1]).toEqual([{ op: 'set', path: ['preference'], value: id }]) await fiber.dispose() expect(locale.getSnapshot().locales.map(option => option.id)).toEqual(['zh', 'en']) - expect(() => { locale.setLocale('ja') }).toThrow('not registered') - const remove = locale.addLanguage({ id: 'ja', label: '日本語', fallback: 'en' }) - locale.setLocale('ja') + expect(() => { locale.setLocale(id) }).toThrow('not registered') + const remove = locale.addLanguage({ id, label, fallback: 'en' }) + locale.setLocale(id) expect(locale.bind('common')('cancel')).toBe('cancel') remove() }) diff --git a/packages/client/locale/tests/document-language.client.spec.ts b/packages/client/locale/tests/document-language.client.spec.ts index 9e72b5149c..a513d988b5 100644 --- a/packages/client/locale/tests/document-language.client.spec.ts +++ b/packages/client/locale/tests/document-language.client.spec.ts @@ -103,7 +103,7 @@ describe('document language', () => { const observer = new MutationObserver(() => {}) observer.observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] }) try { - const remove = locale.addLanguage({ id: 'fr', label: 'Français', fallback: 'en' }) + const remove = locale.addLanguage({ id: 'it', label: 'Italiano', fallback: 'en' }) expect(observer.takeRecords()).toHaveLength(0) remove() expect(observer.takeRecords()).toHaveLength(0) diff --git a/packages/client/modules/locale/de.json b/packages/client/modules/locale/de.json new file mode 100644 index 0000000000..4e5c22fba2 --- /dev/null +++ b/packages/client/modules/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Module", + "description": "Verwalten Sie Browser-Plug-in-Module und ihre Ladeabhängigkeiten." + } +} diff --git a/packages/client/modules/locale/es.json b/packages/client/modules/locale/es.json new file mode 100644 index 0000000000..f005ac4417 --- /dev/null +++ b/packages/client/modules/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "módulos", + "description": "Administre los módulos de complementos del navegador y sus dependencias de carga." + } +} diff --git a/packages/client/modules/locale/fr.json b/packages/client/modules/locale/fr.json new file mode 100644 index 0000000000..29801bfe86 --- /dev/null +++ b/packages/client/modules/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "modules", + "description": "Gérez les modules de plug-in du navigateur et leurs dépendances de chargement." + } +} diff --git a/packages/client/modules/locale/pt.json b/packages/client/modules/locale/pt.json new file mode 100644 index 0000000000..167cc3f507 --- /dev/null +++ b/packages/client/modules/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "módulos", + "description": "Gerencie módulos de plug-in do navegador e suas dependências de carregamento." + } +} diff --git a/packages/client/modules/locale/ru.json b/packages/client/modules/locale/ru.json new file mode 100644 index 0000000000..67041fde84 --- /dev/null +++ b/packages/client/modules/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "модули", + "description": "Управляйте подключаемыми модулями браузера и их зависимостями при загрузке." + } +} diff --git a/packages/client/product-analytics/locale/de.json b/packages/client/product-analytics/locale/de.json new file mode 100644 index 0000000000..fb577f04ad --- /dev/null +++ b/packages/client/product-analytics/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Statistiken zur Desktop-Nutzung", + "description": "Sammeln Sie bestimmte Interaktionsereignisse auf dem Desktop und übermitteln Sie sie an den Produktstatistikdienst." + } +} diff --git a/packages/client/product-analytics/locale/es.json b/packages/client/product-analytics/locale/es.json new file mode 100644 index 0000000000..838528b1c4 --- /dev/null +++ b/packages/client/product-analytics/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Estadísticas de uso de escritorio", + "description": "Recopile eventos de interacción específicos en el escritorio y envíelos al servicio de estadísticas del producto." + } +} diff --git a/packages/client/product-analytics/locale/fr.json b/packages/client/product-analytics/locale/fr.json new file mode 100644 index 0000000000..2edef0a2b3 --- /dev/null +++ b/packages/client/product-analytics/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Statistiques d'utilisation du bureau", + "description": "Collectez les événements d'interaction spécifiés sur le bureau et soumettez-les au service de statistiques du produit." + } +} diff --git a/packages/client/product-analytics/locale/pt.json b/packages/client/product-analytics/locale/pt.json new file mode 100644 index 0000000000..de66d5a74b --- /dev/null +++ b/packages/client/product-analytics/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Estatísticas de uso da área de trabalho", + "description": "Colete eventos de interação especificados na área de trabalho e envie-os ao serviço de estatísticas do produto." + } +} diff --git a/packages/client/product-analytics/locale/ru.json b/packages/client/product-analytics/locale/ru.json new file mode 100644 index 0000000000..f089774778 --- /dev/null +++ b/packages/client/product-analytics/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Статистика использования рабочего стола", + "description": "Собирайте указанные события взаимодействия на рабочем столе и отправляйте их в сервис статистики продукта." + } +} diff --git a/packages/client/resources/locale/de.json b/packages/client/resources/locale/de.json new file mode 100644 index 0000000000..c5d2e7508e --- /dev/null +++ b/packages/client/resources/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Ressourcen", + "description": "Lösen Sie Ressourcenadressen wie Dateien in Inhalte auf, die von der Schnittstelle gelesen werden können." + } +} diff --git a/packages/client/resources/locale/es.json b/packages/client/resources/locale/es.json new file mode 100644 index 0000000000..a09f476f6d --- /dev/null +++ b/packages/client/resources/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "recursos", + "description": "Resuelva direcciones de recursos, como archivos, en contenido que la interfaz pueda leer." + } +} diff --git a/packages/client/resources/locale/fr.json b/packages/client/resources/locale/fr.json new file mode 100644 index 0000000000..4c02e4e9fe --- /dev/null +++ b/packages/client/resources/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ressources", + "description": "Résolvez les adresses de ressources telles que les fichiers en contenu pouvant être lu par l'interface." + } +} diff --git a/packages/client/resources/locale/pt.json b/packages/client/resources/locale/pt.json new file mode 100644 index 0000000000..9b5f1c60fa --- /dev/null +++ b/packages/client/resources/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "recursos", + "description": "Resolva endereços de recursos, como arquivos, em conteúdo que possa ser lido pela interface." + } +} diff --git a/packages/client/resources/locale/ru.json b/packages/client/resources/locale/ru.json new file mode 100644 index 0000000000..e5571b2d32 --- /dev/null +++ b/packages/client/resources/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ресурсы", + "description": "Преобразуйте адреса ресурсов, такие как файлы, в контент, который может быть прочитан интерфейсом." + } +} diff --git a/packages/client/shortcuts/locale/de.json b/packages/client/shortcuts/locale/de.json new file mode 100644 index 0000000000..0de9fbe7f1 --- /dev/null +++ b/packages/client/shortcuts/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Abkürzungen", + "description": "Registrieren Sie Tastenkombinationen für Anwendungen und verteilen Sie Tastenoperationen auf entsprechende Funktionen." + } +} diff --git a/packages/client/shortcuts/locale/es.json b/packages/client/shortcuts/locale/es.json new file mode 100644 index 0000000000..e6d9faac53 --- /dev/null +++ b/packages/client/shortcuts/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "atajos", + "description": "Registre las teclas de acceso directo de la aplicación y distribuya las operaciones clave a las funciones correspondientes." + } +} diff --git a/packages/client/shortcuts/locale/fr.json b/packages/client/shortcuts/locale/fr.json new file mode 100644 index 0000000000..85dcaf0f78 --- /dev/null +++ b/packages/client/shortcuts/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "raccourcis", + "description": "Enregistrez les touches de raccourci de l'application et distribuez les opérations clés aux fonctions correspondantes." + } +} diff --git a/packages/client/shortcuts/locale/pt.json b/packages/client/shortcuts/locale/pt.json new file mode 100644 index 0000000000..10f9714ea8 --- /dev/null +++ b/packages/client/shortcuts/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "atalhos", + "description": "Registre teclas de atalho do aplicativo e distribua as operações das teclas para as funções correspondentes." + } +} diff --git a/packages/client/shortcuts/locale/ru.json b/packages/client/shortcuts/locale/ru.json new file mode 100644 index 0000000000..eedd6069c5 --- /dev/null +++ b/packages/client/shortcuts/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "ярлыки", + "description": "Зарегистрируйте сочетания клавиш приложения и распределите ключевые операции по соответствующим функциям." + } +} diff --git a/packages/client/ui-agent-preset/locale/de.json b/packages/client/ui-agent-preset/locale/de.json new file mode 100644 index 0000000000..bb0817b9bb --- /dev/null +++ b/packages/client/ui-agent-preset/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Standardschnittstelle des Agenten", + "description": "Wählen Sie Agent-Standardeinstellungen für aktuelle und neue Sitzungen aus und bearbeiten Sie die von den Standardeinstellungen verwendeten Plugins." + } +} diff --git a/packages/client/ui-agent-preset/locale/es.json b/packages/client/ui-agent-preset/locale/es.json new file mode 100644 index 0000000000..71f9b56076 --- /dev/null +++ b/packages/client/ui-agent-preset/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz predeterminada del agente", + "description": "Seleccione los valores predeterminados del agente para las sesiones actuales y nuevas, y edite los complementos utilizados por los valores predeterminados." + } +} diff --git a/packages/client/ui-agent-preset/locale/fr.json b/packages/client/ui-agent-preset/locale/fr.json new file mode 100644 index 0000000000..55d48a7007 --- /dev/null +++ b/packages/client/ui-agent-preset/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface par défaut de l'agent", + "description": "Sélectionnez les valeurs par défaut de l'agent pour les sessions actuelles et les nouvelles, et modifiez les plugins utilisés par les valeurs par défaut." + } +} diff --git a/packages/client/ui-agent-preset/locale/pt.json b/packages/client/ui-agent-preset/locale/pt.json new file mode 100644 index 0000000000..794e0ca90c --- /dev/null +++ b/packages/client/ui-agent-preset/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface padrão do agente", + "description": "Selecione Padrões do agente para sessões atuais e novas e edite os plug-ins usados pelos padrões." + } +} diff --git a/packages/client/ui-agent-preset/locale/ru.json b/packages/client/ui-agent-preset/locale/ru.json new file mode 100644 index 0000000000..5ee3dcb323 --- /dev/null +++ b/packages/client/ui-agent-preset/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс агента по умолчанию", + "description": "Выберите параметры агента по умолчанию для текущего и нового сеансов и отредактируйте плагины, используемые по умолчанию." + } +} diff --git a/packages/client/ui-agent-preset/src/client/guide-locales.ts b/packages/client/ui-agent-preset/src/client/guide-locales.ts index ff5a5b5529..d92283aad2 100644 --- a/packages/client/ui-agent-preset/src/client/guide-locales.ts +++ b/packages/client/ui-agent-preset/src/client/guide-locales.ts @@ -173,3 +173,378 @@ export const guideJa = { 'guideCordisExplanation': '### 作成できるもの\n\nクリエイターモードには標準的なタスクツールに加え、ランタイムの検査、Cordis プラグインやエージェントプリセットの作成ガイドが含まれます。機能や UI を追加するプラグインを作成することも、特定の作業向けにツールとプロンプトを組み合わせたプリセットを作成することもできます。\n\n### プラグインとモード\n\nプラグインはツール、サービス接続、UI エントリなど、DSH に機能を追加します。モードはエージェントプリセットであり、ツールを選択し、タスク内でのエージェントの動作を定義します。プラグインはカスタムプリセットに含めることができます。\n\n### 結果を反映させる方法\n\nソースコードを生成するだけでなく、エージェントにインストールと確認を依頼してください。プラグインは変更内容によっては即座に読み込まれるか、再起動が必要になる場合があります。新しく作成したプリセットは、新しいタスクを始めるときに選択されます。', 'guideCordisUsage': '### UI を追加する\n\n> サイドバーにプロジェクトメモのエントリを追加する DSH プラグインを作成してください。このワークスペース内の Markdown ファイルを閲覧でき、選択したメモをプレビューできるようにしてください。インストールしてページが開くことを確認してください。\n\n期待される出力: 動作するエントリとプレビューページを備えた、インストール済みのプラグイン。残っている有効化手順があればそれも含みます。\n\n### ツールを追加する\n\n> このプロジェクトのテストレポートを読み、失敗したテストを要約するツールを備えたプラグインを作成してください。登録し、サンプルのレポートで確認してください。\n\n期待される出力: 呼び出し可能なツールを備えたプラグインと、確認済みのサンプル呼び出し。\n\n### 独自のモードを作成する\n\n> 標準モードをベースに「コードレビュー」モードを作成してください。潜在的なバグとテストの不足を優先し、ファイルパスと行番号を示し、ファイルを変更する前に確認を求めるようにしてください。選択可能なプリセットとして保存してください。\n\n期待される出力: 新しいタスク用のカスタムプリセット。これらのレビュー指示はエージェントを導くものであり、実行できる操作は権限設定によって決まります。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const guideEs = { + modeExplanation: 'Detalles del modo', + howToUse: 'como usar', + guideSections: 'Secciones de guía', + guideExampleTask: 'Tarea de ejemplo', + guideCopy: 'Copiar', + guideCopied: 'Copiado', + guideFootnotes: 'Notas a pie de página', + guideStandardIntro: 'Elija el modo Estándar al iniciar una nueva tarea. Describe lo que quieres lograr, señala los archivos relevantes y explica cómo verificar el resultado.', + guideStandardExplanation: [ + '### Cómo funciona', + 'El agente llama a herramientas directamente para leer y editar archivos, buscar y ejecutar comandos de terminal. Incluye habilidades, planificación, objetivos, subagentes, flujos de trabajo, gestión de complementos y compactación de contexto.', + '### Cuándo elegirlo', + 'Comience aquí para la codificación, el trabajo con archivos y la investigación diaria. El modo estándar también puede escribir scripts y procesar archivos en lotes. PTC cambia la forma en que se organizan las llamadas a herramientas; no es necesario para tareas por lotes.', + ].join('\n\n\n\n'), + guideStandardUsage: [ + '### Corregir un error', + '> Descubra por qué enviar el formulario de búsqueda dos veces hace que los resultados desaparezcan. Arréglalo y ejecuta las pruebas pertinentes. Explique la causa y qué cambió.', + 'Resultado esperado: un cambio de código, los resultados de las pruebas relevantes y una explicación de la causa.', + '### Organizar notas del proyecto', + '> Lea las Markdown notas de este proyecto. Resuma las decisiones acordadas y las preguntas abiertas, con enlaces a los archivos fuente.', + 'Resultado esperado: un resumen con referencias que puede comparar con las notas originales.', + ].join('\n\n\n\n'), + guidePtcIntro: 'Elija el modo PTC al iniciar una nueva tarea. Especifique los archivos de entrada, las reglas de procesamiento y el formato de salida. El agente escribe el código.', + guidePtcExplanation: [ + '### Cómo se llaman las herramientas', + 'PTC significa llamada a herramientas programáticas. En este ajuste preestablecido integrado, el agente usa run_code para escribir un programa TypeScript que llama a herramientas a través de un SDK generado. El programa puede utilizar bucles, condiciones, manejo de errores y llamadas simultáneas cuando corresponda.', + '### Lo que llega al modelo', + 'Los resultados de la herramienta llegan primero al programa, que puede filtrarlos y combinarlos. El modelo recibe lo que el programa imprime o devuelve; Los resultados de la imagen se adjuntan por separado. Las llamadas a herramientas anidadas aún se registran y permanecen sujetas a los permisos de la herramienta.', + '### Comparado con el modo Estándar', + 'Ambos modos pueden manejar tareas de codificación y por lotes. El modo estándar expone herramientas individuales directamente; PTC organiza llamadas a herramientas en código. El ajuste preestablecido actual PTC deja las herramientas de administración de complementos y flujo de trabajo deshabilitadas. La velocidad y el uso de tokens dependen de la tarea y de cómo el programa maneja sus resultados.', + ].join('\n\n\n\n'), + guidePtcUsage: [ + '### Verifique un conjunto de archivos de configuración', + '> Verifique todos los JSON archivos en configs/. Enumere los campos obligatorios faltantes y los valores no válidos en el archivo Schema.json. Guarde un CSV con una fila por número. Incluya archivos ilegibles en el informe y siga revisando el resto. Deje los archivos originales sin cambios.', + 'Resultado esperado: un resumen del problema y un informe CSV. El programa puede repetir las mismas comprobaciones, gestionar fallos individuales y recopilar los resultados.', + '### Resumir registros de errores', + '> Analizar los archivos de registro en logs/. Agrupe errores por servicio y tipo de error. Muestra los diez grupos más frecuentes y un ejemplo de cada uno. Guarde los recuentos completos en un CSV.', + 'Resultado esperado: los principales grupos de errores y una tabla de recuento completa. Los datos intermedios se pueden agregar en el programa antes de que el resumen llegue al modelo.', + ].join('\n\n\n\n'), + guideMinimalIntro: 'Elija el modo mínimo para una nueva tarea. Para realizar una comparación, mantenga constantes el modelo, los permisos, la entrada y el estado inicial del espacio de trabajo en todas las ejecuciones.', + guideMinimalExplanation: [ + '### ¿Qué está incluido?', + 'Una herramienta de shell persistente, read_image, y un indicador de sistema fijo. La lectura de imágenes requiere un modelo compatible con imágenes. La historia más antigua se compacta automáticamente bajo la presión del contexto; use /compact mientras el agente esté inactivo para compactarlo manualmente. El ajuste preestablecido integrado no carga las habilidades, la planificación ni el contexto de tiempo de ejecución estándar.', + '### Cuándo elegirlo', + 'Úselo como base para experimentos y comparaciones. Todavía puede leer archivos y ejecutar scripts mediante comandos de shell, pero ofrece menos formas integradas de gestionar una tarea larga. Menos herramientas no necesariamente lo hace más fácil para un principiante.', + ].join('\n\n\n\n'), + guideMinimalUsage: [ + '### Comparar el rendimiento en una pequeña corrección de errores', + '> Ejecute las pruebas para este proyecto, encuentre la causa del error y realice la solución más pequeña. Ejecute las pruebas pertinentes nuevamente e informe el resultado.', + 'Ejecute la misma tarea por separado en los modos Estándar y Mínimo desde el mismo estado inicial. Compare la finalización de tareas, las llamadas a herramientas y los cambios resultantes. El modo mínimo realiza el trabajo a través de comandos de terminal.', + ].join('\n\n\n\n'), + guideCordisIntro: 'Elija el modo Creador para una nueva tarea. Describe la capacidad que deseas, dónde debería aparecer y cómo la verificarás.', + guideCordisExplanation: [ + '### Lo que puedes crear', + 'El modo Creador incluye las herramientas de tareas estándar además de inspección del tiempo de ejecución y orientación para crear Cordis complementos y ajustes preestablecidos de agentes. Puede crear un complemento que agregue una capacidad o interfaz de usuario, o un ajuste preestablecido que combine herramientas e indicaciones para un trabajo en particular.', + '### Complementos y modos', + 'Un complemento agrega capacidades a DSH, como una herramienta, una conexión de servicio o una entrada de UI. Un modo es un ajuste preestablecido del agente que selecciona herramientas y define cómo trabaja el agente en una tarea. Se puede incluir un complemento en un ajuste preestablecido personalizado.', + '### Cómo surte efecto el resultado', + 'Pídale al agente que instale y verifique el resultado, no solo que genere el código fuente. Un complemento puede cargarse inmediatamente o requerir un reinicio, dependiendo de lo que cambie. Se selecciona un ajuste preestablecido recién creado al iniciar una nueva tarea.', + ].join('\n\n\n\n'), + guideCordisUsage: [ + '### Agregar una interfaz de usuario', + '> Cree un complemento DSH que agregue una entrada de notas del proyecto a la barra lateral. Permítanme explorar los Markdown archivos en este espacio de trabajo y obtener una vista previa de una nota seleccionada. Instálalo y verifica que se abre la página.', + 'Resultado esperado: un complemento instalado con una entrada funcional y una página de vista previa, además de los pasos de activación restantes.', + '### Agregar una herramienta', + '> Cree un complemento con una herramienta que lea el informe de prueba de este proyecto y resuma las pruebas fallidas. Regístrelo y verifíquelo con un informe de muestra.', + 'Resultado esperado: un complemento con una herramienta invocable y una llamada de muestra verificada.', + '### Crear mi propio modo', + '> Cree un modo de “Revisión de código” basado en el modo Estándar. Haga que priorice posibles errores y pruebe las lagunas, cite rutas y líneas de archivos y pregunte antes de modificar archivos. Guárdelo como un ajuste preestablecido seleccionable.', + 'Resultado esperado: un ajuste preestablecido personalizado para nuevas tareas. Estas instrucciones de revisión guían al agente; La configuración de permisos determina qué acciones puede ejecutar.', + ].join('\n\n\n\n'), +} satisfies typeof guideEn + +/** French dictionary, checked against the English keys. */ +export const guideFr = { + modeExplanation: 'Détails du mode', + howToUse: 'Comment utiliser', + guideSections: 'Sections du guide', + guideExampleTask: 'Exemple de tâche', + guideCopy: 'Copier', + guideCopied: 'Copié', + guideFootnotes: 'Notes de bas de page', + guideStandardIntro: 'Choisissez le mode Standard lorsque vous démarrez une nouvelle tâche. Décrivez ce que vous souhaitez accomplir, pointez vers les fichiers pertinents et expliquez comment vérifier le résultat.', + guideStandardExplanation: [ + '### Comment ça marche', + "L'agent appelle directement des outils pour lire et modifier des fichiers, rechercher et exécuter des commandes de terminal. Il comprend les compétences, la planification, les objectifs, les sous-agents, les flux de travail, la gestion des plugins et le compactage du contexte.", + '### Quand le choisir', + "Commencez ici pour le codage quotidien, le travail sur les fichiers et la recherche. Le mode standard peut également écrire des scripts et traiter des fichiers par lots. PTC modifie la façon dont les appels d'outils sont organisés ; il n'est pas requis pour les tâches par lots.", + ].join('\n\n\n\n'), + guideStandardUsage: [ + "### Correction d'un bug", + '> Découvrez pourquoi le fait de soumettre deux fois le formulaire de recherche fait disparaître les résultats. Corrigez-le et exécutez les tests appropriés. Expliquez la cause et ce qui a changé.', + 'Résultat attendu : un changement de code, les résultats des tests pertinents et une explication de la cause.', + '### Organiser les notes de projet', + '> Lisez les Markdown notes de ce projet. Résumez les décisions convenues et les questions ouvertes, avec des liens vers les fichiers sources.', + 'Résultat attendu : un résumé avec des références que vous pouvez vérifier par rapport aux notes originales.', + ].join('\n\n\n\n'), + guidePtcIntro: "Choisissez le mode PTC lorsque vous démarrez une nouvelle tâche. Spécifiez les fichiers d'entrée, les règles de traitement et le format de sortie. L'agent écrit le code.", + guidePtcExplanation: [ + '### Comment sont appelés les outils', + "PTC signifie appel d'outil de programmation. Dans ce préréglage intégré, l'agent utilise run_code pour écrire un programme TypeScript qui appelle des outils via un SDK généré. Le programme peut utiliser des boucles, des conditions, une gestion des erreurs et des appels simultanés, le cas échéant.", + '### Ce qui arrive au modèle', + "Les résultats des outils parviennent d’abord au programme, qui peut les filtrer et les combiner. Le modèle reçoit ce que le programme imprime ou renvoie ; les résultats des images sont joints séparément. Les appels d'outils imbriqués sont toujours enregistrés et restent soumis aux autorisations de l'outil.", + '### Par rapport au mode Standard', + "Les deux modes peuvent gérer les tâches de codage et par lots. Le mode standard expose directement les outils individuels ; PTC organise les appels d'outils dans le code. Le préréglage PTC actuel laisse les outils de gestion de flux de travail et de plugins désactivés. La vitesse et l'utilisation des jetons dépendent de la tâche et de la manière dont le programme gère ses résultats.", + ].join('\n\n\n\n'), + guidePtcUsage: [ + '### Vérifier un ensemble de fichiers de configuration', + '> Vérifiez tous les JSON fichiers dans configs/. Répertoriez les champs obligatoires manquants et les valeurs non valides par rapport à schema.json. Enregistrez un CSV avec une ligne par numéro. Incluez les fichiers illisibles dans le rapport et continuez à vérifier le reste. Laissez les fichiers originaux inchangés.', + 'Résultat attendu : un résumé du problème et un rapport CSV. Le programme peut répéter les mêmes vérifications, gérer les échecs individuels et collecter les résultats.', + "### Résumer les journaux d'erreurs", + '> Analysez les fichiers journaux dans logs/. Regroupez les erreurs par service et type d’erreur. Montrez les dix groupes les plus fréquents et un exemple de chacun. Enregistrez les décomptes complets dans un CSV.', + "Résultat attendu : les principaux groupes d’erreurs et un tableau de décompte complet. Les données intermédiaires peuvent être agrégées dans le programme avant que le résumé n'atteigne le modèle.", + ].join('\n\n\n\n'), + guideMinimalIntro: 'Choisissez le mode Minimal pour une nouvelle tâche. À des fins de comparaison, maintenez le modèle, les autorisations, l’entrée et l’état de départ de l’espace de travail constants d’une exécution à l’autre.', + guideMinimalExplanation: [ + '### Ce qui est inclus', + "Un outil shell persistant, read_image et une invite système fixe. La lecture d'images nécessite un modèle compatible avec les images. L’historique plus ancien est automatiquement compacté sous la pression du contexte ; utilisez /compact pendant que l'agent est inactif pour le compacter manuellement. Le préréglage intégré ne charge pas les compétences, la planification ou le contexte d'exécution standard.", + '### Quand le choisir', + 'Utilisez-le comme base de référence pour des expériences et des comparaisons. Il peut toujours lire des fichiers et exécuter des scripts via des commandes shell, mais offre moins de moyens intégrés pour gérer une tâche longue. Moins d’outils ne facilite pas nécessairement la tâche d’un débutant.', + ].join('\n\n\n\n'), + guideMinimalUsage: [ + '### Comparez les performances sur une petite correction de bug', + "> Exécutez les tests pour ce projet, trouvez la cause de l'échec et effectuez le moindre correctif. Exécutez à nouveau les tests pertinents et rapportez le résultat.", + "Exécutez la même tâche séparément en modes Standard et Minimal à partir du même état de départ. Comparez l'achèvement des tâches, les appels d'outils et les modifications qui en résultent. Le mode minimal effectue le travail via les commandes du terminal.", + ].join('\n\n\n\n'), + guideCordisIntro: 'Choisissez le mode Créateur pour une nouvelle tâche. Décrivez la fonctionnalité souhaitée, où elle doit apparaître et comment vous la vérifierez.', + guideCordisExplanation: [ + '### Ce que vous pouvez créer', + "Le mode Créateur inclut les outils de tâches standard, ainsi qu'une inspection d'exécution et des conseils pour la création de Cordis plug-ins et préréglages d'agent. Il peut créer un plugin qui ajoute une fonctionnalité ou une interface utilisateur, ou un préréglage combinant des outils et des invites pour une tâche particulière.", + '### Plugins et modes', + "Un plug-in ajoute des fonctionnalités à DSH, telles qu'un outil, une connexion à un service ou une entrée d'interface utilisateur. Un mode est un préréglage d'agent qui sélectionne les outils et définit la manière dont l'agent fonctionne dans une tâche. Un plugin peut être inclus dans un préréglage personnalisé.", + '### Comment le résultat prend effet', + "Demandez à l'agent d'installer et de vérifier le résultat, pas seulement de générer le code source. Un plugin peut se charger immédiatement ou nécessiter un redémarrage, en fonction de ce qu'il modifie. Un préréglage nouvellement créé est sélectionné lors du démarrage d'une nouvelle tâche.", + ].join('\n\n\n\n'), + guideCordisUsage: [ + '### Ajouter une interface utilisateur', + "> Créez un plugin DSH qui ajoute une entrée de notes de projet à la barre latérale. Permettez-moi de parcourir les Markdown fichiers de cet espace de travail et de prévisualiser une note sélectionnée. Installez-le et vérifiez que la page s'ouvre.", + "Résultat attendu : un plugin installé avec une page d'entrée et d'aperçu fonctionnelle, ainsi que toutes les étapes d'activation restantes.", + '### Ajouter un outil', + '> Créez un plugin avec un outil qui lit le rapport de test de ce projet et résume les tests échoués. Enregistrez-le et vérifiez-le avec un exemple de rapport.', + "Résultat attendu : un plugin avec un outil appelable et un exemple d'appel vérifié.", + '### Créer mon propre mode', + '> Créer un mode « Revue de code » basé sur le mode Standard. Demandez-lui de prioriser les bogues potentiels et de tester les lacunes, de citer les chemins et les lignes des fichiers et de demander avant de modifier les fichiers. Enregistrez-le en tant que préréglage sélectionnable.', + "Résultat attendu : un préréglage personnalisé pour les nouvelles tâches. Ces instructions de révision guident l'agent ; les paramètres d'autorisation déterminent les actions qu'il peut exécuter.", + ].join('\n\n\n\n'), +} satisfies typeof guideEn + +/** German dictionary, checked against the English keys. */ +export const guideDe = { + modeExplanation: 'Modusdetails', + howToUse: 'Wie zu verwenden', + guideSections: 'Leitfadenabschnitte', + guideExampleTask: 'Beispielaufgabe', + guideCopy: 'Kopieren', + guideCopied: 'Kopiert', + guideFootnotes: 'Fußnoten', + guideStandardIntro: 'Wählen Sie den Standardmodus, wenn Sie eine neue Aufgabe starten. Beschreiben Sie, was Sie erreichen möchten, zeigen Sie auf die relevanten Dateien und erklären Sie, wie Sie das Ergebnis überprüfen können.', + guideStandardExplanation: [ + '### Wie es funktioniert', + 'Der Agent ruft Tools direkt auf, um Dateien zu lesen und zu bearbeiten, zu suchen und Terminalbefehle auszuführen. Es umfasst Fähigkeiten, Planung, Ziele, Subagenten, Arbeitsabläufe, Plugin-Verwaltung und Kontextkomprimierung.', + '### Wann man es wählt', + 'Beginnen Sie hier mit alltäglicher Codierung, Dateiarbeit und Recherche. Im Standardmodus können auch Skripts geschrieben und Dateien stapelweise verarbeitet werden. PTC ändert die Organisation von Toolaufrufen; Für Batch-Aufgaben ist es nicht erforderlich.', + ].join('\n\n\n\n'), + guideStandardUsage: [ + '### Einen Fehler beheben', + '> Finden Sie heraus, warum das zweimalige Absenden des Suchformulars dazu führt, dass die Ergebnisse verschwinden. Beheben Sie das Problem und führen Sie die entsprechenden Tests durch. Erklären Sie die Ursache und was sich geändert hat.', + 'Erwartete Ausgabe: eine Codeänderung, die relevanten Testergebnisse und eine Erklärung der Ursache.', + '### Projektnotizen organisieren', + '> Lesen Sie die Markdown-Hinweise in diesem Projekt. Fassen Sie die vereinbarten Entscheidungen und offenen Fragen zusammen, mit Links zu den Quelldateien.', + 'Erwartete Ausgabe: eine Zusammenfassung mit Referenzen, die Sie mit den Originalnotizen vergleichen können.', + ].join('\n\n\n\n'), + guidePtcIntro: 'Wählen Sie den Modus PTC, wenn Sie eine neue Aufgabe starten. Geben Sie die Eingabedateien, Verarbeitungsregeln und das Ausgabeformat an. Der Agent schreibt den Code.', + guidePtcExplanation: [ + '### Wie Werkzeuge heißen', + 'PTC bedeutet Programmatic Tool Calling. In dieser integrierten Voreinstellung verwendet der Agent run_code, um ein TypeScript-Programm zu schreiben, das Tools über ein generiertes SDK aufruft. Das Programm kann gegebenenfalls Schleifen, Bedingungen, Fehlerbehandlung und gleichzeitige Aufrufe verwenden.', + '### Was das Modell erreicht', + 'Werkzeugergebnisse gelangen zunächst in das Programm, das diese filtern und kombinieren kann. Das Modell empfängt, was das Programm ausgibt oder zurückgibt; Bildergebnisse sind separat beigefügt. Verschachtelte Werkzeugaufrufe werden weiterhin aufgezeichnet und unterliegen weiterhin den Werkzeugberechtigungen.', + '### Im Vergleich zum Standardmodus', + 'Beide Modi können Codierungs- und Batch-Aufgaben bewältigen. Im Standardmodus werden einzelne Tools direkt verfügbar gemacht. PTC organisiert Toolaufrufe im Code. Die aktuelle Voreinstellung PTC lässt die Workflow- und Plugin-Verwaltungstools deaktiviert. Geschwindigkeit und Token-Nutzung hängen von der Aufgabe ab und davon, wie das Programm mit seinen Ergebnissen umgeht.', + ].join('\n\n\n\n'), + guidePtcUsage: [ + '### Überprüfen Sie eine Reihe von Konfigurationsdateien', + '> Überprüfen Sie alle JSON Dateien in configs/. Fehlende Pflichtfelder und ungültige Werte für schema.json auflisten. Speichern Sie eine CSV-Datei mit einer Zeile pro Ausgabe. Fügen Sie unleserliche Dateien in den Bericht ein und überprüfen Sie den Rest weiter. Lassen Sie die Originaldateien unverändert.', + 'Erwartete Ausgabe: eine Problemzusammenfassung und ein CSV-Bericht. Das Programm kann dieselben Prüfungen wiederholen, einzelne Fehler behandeln und die Ergebnisse sammeln.', + '### Fehlerprotokolle zusammenfassen', + '> Analysieren Sie die Protokolldateien in logs/. Gruppieren Sie Fehler nach Dienst und Fehlertyp. Zeigen Sie die zehn häufigsten Gruppen und jeweils ein Beispiel. Speichern Sie die vollständigen Zählungen in einer CSV-Datei.', + 'Erwartete Ausgabe: die Top-Fehlergruppen und eine vollständige Zähltabelle. Zwischendaten können im Programm aggregiert werden, bevor die Zusammenfassung das Modell erreicht.', + ].join('\n\n\n\n'), + guideMinimalIntro: 'Wählen Sie den Minimalmodus für eine neue Aufgabe. Halten Sie für einen Vergleich das Modell, die Berechtigungen, die Eingabe und den Status des Startarbeitsbereichs über alle Ausführungen hinweg konstant.', + guideMinimalExplanation: [ + '### Was ist enthalten', + 'Ein persistentes Shell-Tool, read_image, und eine feste Systemeingabeaufforderung. Das Lesen von Bildern erfordert ein bildfähiges Modell. Der ältere Verlauf wird unter Kontextdruck automatisch komprimiert; Verwenden Sie /compact, während der Agent inaktiv ist, um ihn manuell zu komprimieren. Die integrierte Voreinstellung lädt weder Skills, Planung noch den Standardlaufzeitkontext.', + '### Wann man es wählt', + 'Verwenden Sie es als Grundlage für Experimente und Vergleiche. Es kann weiterhin Dateien lesen und Skripte über Shell-Befehle ausführen, bietet jedoch weniger integrierte Möglichkeiten zur Verwaltung einer langen Aufgabe. Weniger Werkzeuge machen es einem Anfänger nicht unbedingt einfacher.', + ].join('\n\n\n\n'), + guideMinimalUsage: [ + '### Vergleichen Sie die Leistung mit einer kleinen Fehlerbehebung', + '> Führen Sie die Tests für dieses Projekt durch, finden Sie die Fehlerursache und nehmen Sie die kleinste Lösung vor. Führen Sie die entsprechenden Tests erneut durch und melden Sie das Ergebnis.', + 'Führen Sie dieselbe Aufgabe separat im Standard- und Minimalmodus aus, und zwar im selben Startzustand. Vergleichen Sie die Aufgabenerledigung, Werkzeugaufrufe und die daraus resultierenden Änderungen. Der Minimalmodus führt die Arbeit über Terminalbefehle aus.', + ].join('\n\n\n\n'), + guideCordisIntro: 'Wählen Sie den Erstellermodus für eine neue Aufgabe. Beschreiben Sie die gewünschte Funktion, wo sie angezeigt werden soll und wie Sie sie überprüfen.', + guideCordisExplanation: [ + '### Was Sie erstellen können', + 'Der Erstellermodus umfasst die Standardaufgabentools sowie eine Laufzeitprüfung und Anleitungen zum Erstellen von Cordis-Plugins und Agent-Voreinstellungen. Es kann ein Plugin erstellt werden, das eine Funktion oder Benutzeroberfläche hinzufügt, oder eine Voreinstellung, die Tools und Eingabeaufforderungen für einen bestimmten Job kombiniert.', + '### Plugins und Modi', + 'Ein Plugin fügt DSH Funktionen hinzu, beispielsweise ein Tool, eine Dienstverbindung oder einen UI-Eintrag. Ein Modus ist eine Agentenvoreinstellung, die Tools auswählt und definiert, wie der Agent in einer Aufgabe arbeitet. Ein Plugin kann in eine benutzerdefinierte Voreinstellung eingebunden werden.', + '### Wie das Ergebnis wirksam wird', + 'Bitten Sie den Agenten, das Ergebnis zu installieren und zu überprüfen und nicht nur Quellcode zu generieren. Abhängig von den Änderungen kann ein Plugin sofort geladen werden oder einen Neustart erfordern. Beim Starten einer neuen Aufgabe wird eine neu erstellte Voreinstellung ausgewählt.', + ].join('\n\n\n\n'), + guideCordisUsage: [ + '### Fügen Sie eine Benutzeroberfläche hinzu', + '> Erstellen Sie ein DSH-Plugin, das der Seitenleiste einen Eintrag für Projektnotizen hinzufügt. Lassen Sie mich die Markdown Dateien in diesem Arbeitsbereich durchsuchen und eine Vorschau einer ausgewählten Notiz anzeigen. Installieren Sie es und stellen Sie sicher, dass die Seite geöffnet wird.', + 'Erwartete Ausgabe: ein installiertes Plugin mit einer funktionierenden Einstiegs- und Vorschauseite sowie allen verbleibenden Aktivierungsschritten.', + '### Ein Werkzeug hinzufügen', + '> Erstellen Sie ein Plugin mit einem Tool, das den Testbericht dieses Projekts liest und die fehlgeschlagenen Tests zusammenfasst. Registrieren Sie es und überprüfen Sie es anhand eines Beispielberichts.', + 'Erwartete Ausgabe: ein Plugin mit einem aufrufbaren Tool und einem verifizierten Beispielaufruf.', + '### Erstelle meinen eigenen Modus', + '> Erstellen Sie einen „Code Review“-Modus basierend auf dem Standardmodus. Lassen Sie potenzielle Fehler priorisieren und Lücken testen, geben Sie Dateipfade und -zeilen an und fragen Sie nach, bevor Sie Dateien ändern. Speichern Sie es als auswählbare Voreinstellung.', + 'Erwartete Ausgabe: eine benutzerdefinierte Voreinstellung für neue Aufgaben. Diese Überprüfungsanweisungen leiten den Agenten; Berechtigungseinstellungen bestimmen, welche Aktionen ausgeführt werden können.', + ].join('\n\n\n\n'), +} satisfies typeof guideEn + +/** Russian dictionary, checked against the English keys. */ +export const guideRu = { + modeExplanation: 'Подробности режима', + howToUse: 'Как использовать', + guideSections: 'Разделы руководства', + guideExampleTask: 'Пример задачи', + guideCopy: 'Копировать', + guideCopied: 'Скопировано', + guideFootnotes: 'Сноски', + guideStandardIntro: 'Выбирайте стандартный режим при запуске новой задачи. Опишите, чего вы хотите добиться, укажите соответствующие файлы и объясните, как проверить результат.', + guideStandardExplanation: [ + '### Как это работает', + 'Агент напрямую вызывает инструменты для чтения и редактирования файлов, поиска и выполнения команд терминала. Он включает в себя навыки, планирование, цели, субагенты, рабочие процессы, управление плагинами и сжатие контекста.', + '### Когда это выбирать', + 'Начните здесь ежедневное программирование, работу с файлами и исследования. Стандартный режим также позволяет писать сценарии и обрабатывать файлы в пакетном режиме. PTC изменяет организацию вызовов инструментов; для пакетных задач это не требуется.', + ].join('\n\n\n\n'), + guideStandardUsage: [ + '### Исправить ошибку', + '> Узнайте, почему при двойной отправке формы поиска результаты исчезают. Исправьте это и запустите соответствующие тесты. Объясните причину и что изменилось.', + 'Ожидаемый результат: изменение кода, соответствующие результаты испытаний и объяснение причины.', + '### Организация заметок по проекту', + '> Прочтите примечания Markdown в этом проекте. Кратко изложите согласованные решения и открытые вопросы со ссылками на исходные файлы.', + 'Ожидаемый результат: резюме со ссылками, которые вы можете сверить с исходными заметками.', + ].join('\n\n\n\n'), + guidePtcIntro: 'Выберите режим PTC при запуске новой задачи. Укажите входные файлы, правила обработки и выходной формат. Агент пишет код.', + guidePtcExplanation: [ + '### Как называются инструменты', + 'PTC означает программный вызов инструмента. В этой встроенной настройке агент использует run_code для написания программы TypeScript, которая вызывает инструменты через сгенерированный SDK. Программа может использовать циклы, условия, обработку ошибок и параллельные вызовы, где это необходимо.', + '### Что достигает модели', + 'Результаты инструмента сначала поступают в программу, которая может их фильтровать и комбинировать. Модель получает то, что программа печатает или возвращает; Результаты изображения прилагаются отдельно. Вызовы вложенных инструментов по-прежнему записываются, и для них действуют разрешения инструмента.', + '### По сравнению со стандартным режимом', + 'Оба режима могут выполнять кодирование и пакетные задачи. Стандартный режим напрямую предоставляет доступ к отдельным инструментам; PTC организует вызовы инструментов в коде. Текущая предустановка PTC оставляет инструменты управления рабочим процессом и плагинами отключенными. Скорость и использование токенов зависят от задачи и того, как программа обрабатывает ее результаты.', + ].join('\n\n\n\n'), + guidePtcUsage: [ + '### Проверка набора файлов конфигурации', + '> Проверьте все файлы JSON в configs/. Перечислите отсутствующие обязательные поля и недопустимые значения в файле Schema.json. Сохраните CSV-файл с одной строкой на каждую проблему. Включите в отчет нечитаемые файлы и продолжайте проверять остальные. Оставьте исходные файлы без изменений.', + 'Ожидаемый результат: краткое описание проблемы и отчет в формате CSV. Программа может повторять одни и те же проверки, обрабатывать отдельные сбои и собирать результаты.', + '### Обобщение журналов ошибок', + '> Проанализируйте файлы журналов в logs/. Группируйте ошибки по сервису и типу ошибки. Покажите десять наиболее часто встречающихся групп и по одному примеру из каждой. Сохраните полные данные в формате CSV.', + 'Ожидаемый результат: основные группы ошибок и полная таблица подсчета. Промежуточные данные могут быть агрегированы в программе до того, как сводка достигнет модели.', + ].join('\n\n\n\n'), + guideMinimalIntro: 'Выберите минимальный режим для новой задачи. Для сравнения сохраняйте постоянство модели, разрешений, входных данных и состояния начальной рабочей области во всех запусках.', + guideMinimalExplanation: [ + '### Что включено', + 'Один постоянный инструмент оболочки read_image и фиксированное системное приглашение. Для чтения изображений требуется модель с поддержкой изображений. Старая история автоматически сжимается под давлением контекста; используйте /compact, пока агент простаивает, чтобы сжать его вручную. Встроенная предустановка не загружает навыки, планирование или стандартный контекст времени выполнения.', + '### Когда это выбирать', + 'Используйте его как основу для экспериментов и сравнений. Он по-прежнему может читать файлы и выполнять сценарии с помощью команд оболочки, но предлагает меньше встроенных способов управления длительными задачами. Меньшее количество инструментов не обязательно облегчает работу новичку.', + ].join('\n\n\n\n'), + guideMinimalUsage: [ + '### Сравните производительность с исправлением небольшой ошибки', + '> Запустите тесты для этого проекта, найдите причину сбоя и внесите минимальные исправления. Запустите соответствующие тесты еще раз и сообщите о результате.', + 'Запустите одну и ту же задачу отдельно в стандартном и минимальном режимах из одного и того же начального состояния. Сравните выполнение задач, вызовы инструментов и результирующие изменения. Минимальный режим выполняет работу посредством команд терминала.', + ].join('\n\n\n\n'), + guideCordisIntro: 'Выберите режим «Создатель» для новой задачи. Опишите желаемую возможность, где она должна появиться и как вы ее проверите.', + guideCordisExplanation: [ + '### Что вы можете создать', + 'Режим Creator включает в себя стандартные инструменты задач, а также проверку во время выполнения и рекомендации по созданию подключаемых модулей Cordis и предустановок агента. Он может создать плагин, добавляющий возможности или пользовательский интерфейс, или предустановку, объединяющую инструменты и подсказки для конкретной работы.', + '### Плагины и режимы', + 'Плагин добавляет к DSH возможности, такие как инструмент, подключение к службе или запись пользовательского интерфейса. Режим — это предустановка агента, которая выбирает инструменты и определяет, как агент работает над задачей. Плагин может быть включен в пользовательский пресет.', + '### Как результат вступает в силу', + 'Попросите агента установить и проверить результат, а не просто генерировать исходный код. Плагин может загрузиться немедленно или потребовать перезагрузки, в зависимости от того, что он меняет. Вновь созданный пресет выбирается при запуске новой задачи.', + ].join('\n\n\n\n'), + guideCordisUsage: [ + '### Добавьте пользовательский интерфейс', + '> Создайте плагин DSH, который добавляет запись примечаний к проекту на боковую панель. Позвольте мне просмотреть файлы Markdown в этом рабочем пространстве и просмотреть выбранную заметку. Установите его и убедитесь, что страница открывается.', + 'Ожидаемый результат: установленный плагин с работающей страницей входа и предварительного просмотра, а также все оставшиеся шаги активации.', + '### Добавить инструмент', + '> Создайте плагин с инструментом, который читает отчет об испытаниях этого проекта и суммирует неудачные тесты. Зарегистрируйте его и проверьте с помощью образца отчета.', + 'Ожидаемый результат: плагин с вызываемым инструментом и проверенным примером вызова.', + '### Создать свой режим', + '> Создайте режим «Проверка кода» на основе стандартного режима. Попросите его определить приоритетность потенциальных ошибок и проверить пробелы, указать пути и строки к файлам и спросить, прежде чем изменять файлы. Сохраните его как выбираемую предустановку.', + 'Ожидаемый результат: индивидуальная настройка для новых задач. Эти инструкции по проверке помогут агенту; настройки разрешений определяют, какие действия он может выполнять.', + ].join('\n\n\n\n'), +} satisfies typeof guideEn + +/** Portuguese dictionary, checked against the English keys. */ +export const guidePt = { + modeExplanation: 'Detalhes do modo', + howToUse: 'Como usar', + guideSections: 'Seções do guia', + guideExampleTask: 'Tarefa de exemplo', + guideCopy: 'Copiar', + guideCopied: 'Copiado', + guideFootnotes: 'Notas de rodapé', + guideStandardIntro: 'Escolha o modo Padrão ao iniciar uma nova tarefa. Descreva o que você deseja realizar, indique os arquivos relevantes e explique como verificar o resultado.', + guideStandardExplanation: [ + '### Como funciona', + 'O agente chama ferramentas diretamente para ler e editar arquivos, pesquisar e executar comandos de terminal. Inclui habilidades, planejamento, metas, subagentes, fluxos de trabalho, gerenciamento de plugins e compactação de contexto.', + '### Quando escolher', + 'Comece aqui para codificação diária, trabalho de arquivo e pesquisa. O modo padrão também pode escrever scripts e processar arquivos em lotes. PTC altera a forma como as chamadas de ferramenta são organizadas; não é necessário para tarefas em lote.', + ].join('\n\n\n\n'), + guideStandardUsage: [ + '### Corrigir um bug', + '> Descubra por que enviar o formulário de pesquisa duas vezes faz com que os resultados desapareçam. Corrija-o e execute os testes relevantes. Explique a causa e o que mudou.', + 'Resultado esperado: uma alteração de código, os resultados de teste relevantes e uma explicação da causa.', + '### Organize notas do projeto', + '> Leia as Markdown notas deste projeto. Resuma as decisões acordadas e as questões abertas, com links para os arquivos originais.', + 'Resultado esperado: um resumo com referências que você pode comparar com as notas originais.', + ].join('\n\n\n\n'), + guidePtcIntro: 'Escolha o modo PTC ao iniciar uma nova tarefa. Especifique os arquivos de entrada, regras de processamento e formato de saída. O agente escreve o código.', + guidePtcExplanation: [ + '### Como as ferramentas são chamadas', + 'PTC significa chamada de ferramenta programática. Nesta predefinição integrada, o agente usa run_code para escrever um programa TypeScript que chama ferramentas por meio de um SDK gerado. O programa pode usar loops, condições, tratamento de erros e chamadas simultâneas quando apropriado.', + '### O que chega ao modelo', + 'Os resultados da ferramenta chegam primeiro ao programa, que pode filtrá-los e combiná-los. O modelo recebe o que o programa imprime ou retorna; os resultados da imagem são anexados separadamente. As chamadas de ferramentas aninhadas ainda são gravadas e permanecem sujeitas às permissões da ferramenta.', + '### Comparado com o modo Padrão', + 'Ambos os modos podem lidar com tarefas de codificação e em lote. O modo padrão expõe ferramentas individuais diretamente; PTC organiza chamadas de ferramentas em código. A predefinição PTC atual deixa o fluxo de trabalho e as ferramentas de gerenciamento de plug-ins desativados. A velocidade e o uso do token dependem da tarefa e de como o programa lida com seus resultados.', + ].join('\n\n\n\n'), + guidePtcUsage: [ + '### Verifique um conjunto de arquivos de configuração', + '> Verifique todos os arquivos JSON em configs/. Liste os campos obrigatórios ausentes e os valores inválidos em relação ao schema.json. Salve um CSV com uma linha por problema. Inclua arquivos ilegíveis no relatório e continue verificando o restante. Deixe os arquivos originais inalterados.', + 'Resultado esperado: um resumo do problema e um relatório CSV. O programa pode repetir as mesmas verificações, tratar falhas individuais e coletar os resultados.', + '### Resumir logs de erros', + '> Analise os arquivos de log em logs/. Agrupe erros por serviço e tipo de erro. Mostre os dez grupos mais frequentes e um exemplo de cada. Salve as contagens completas em um CSV.', + 'Resultado esperado: os principais grupos de erros e uma tabela de contagem completa. Os dados intermediários podem ser agregados no programa antes que o resumo chegue ao modelo.', + ].join('\n\n\n\n'), + guideMinimalIntro: 'Escolha o modo Mínimo para uma nova tarefa. Para uma comparação, mantenha o modelo, as permissões, a entrada e o estado inicial do espaço de trabalho constantes em todas as execuções.', + guideMinimalExplanation: [ + '### O que está incluído', + 'Uma ferramenta de shell persistente, read_image, e um prompt de sistema fixo. A leitura de imagens requer um modelo com capacidade de imagem. O histórico mais antigo é compactado automaticamente sob pressão do contexto; use /compact enquanto o agente estiver ocioso para compactá-lo manualmente. A predefinição integrada não carrega habilidades, planejamento ou contexto de tempo de execução padrão.', + '### Quando escolher', + 'Use-o como base para experimentos e comparações. Ele ainda pode ler arquivos e executar scripts por meio de comandos shell, mas oferece menos maneiras integradas de gerenciar uma tarefa longa. Menos ferramentas não torna necessariamente mais fácil para um iniciante.', + ].join('\n\n\n\n'), + guideMinimalUsage: [ + '### Compare o desempenho em uma pequena correção de bug', + '> Execute os testes para este projeto, encontre a causa da falha e faça a menor correção. Execute os testes relevantes novamente e relate o resultado.', + 'Execute a mesma tarefa separadamente nos modos Padrão e Mínimo a partir do mesmo estado inicial. Compare a conclusão da tarefa, as chamadas de ferramentas e as alterações resultantes. O modo mínimo executa o trabalho por meio de comandos de terminal.', + ].join('\n\n\n\n'), + guideCordisIntro: 'Escolha o modo Criador para uma nova tarefa. Descreva o recurso desejado, onde ele deve aparecer e como você o verificará.', + guideCordisExplanation: [ + '### O que você pode criar', + 'O modo Criador inclui ferramentas de tarefas padrão, além de inspeção de tempo de execução e orientação para criação de Cordis plug-ins e predefinições de agente. Ele pode criar um plug-in que adicione um recurso ou UI, ou uma predefinição que combine ferramentas e prompts para um trabalho específico.', + '### Plug-ins e modos', + 'Um plug-in adiciona recursos a DSH, como uma ferramenta, uma conexão de serviço ou uma entrada de IU. Um modo é uma predefinição de agente que seleciona ferramentas e define como o agente trabalha em uma tarefa. Um plugin pode ser incluído em uma predefinição personalizada.', + '### Como o resultado entra em vigor', + 'Peça ao agente para instalar e verificar o resultado, não apenas gerar o código-fonte. Um plug-in pode carregar imediatamente ou exigir uma reinicialização, dependendo do que for alterado. Uma predefinição recém-criada é selecionada ao iniciar uma nova tarefa.', + ].join('\n\n\n\n'), + guideCordisUsage: [ + '### Adicionar uma IU', + '> Crie um plug-in DSH que adicione uma entrada de notas do projeto à barra lateral. Deixe-me navegar pelos arquivos Markdown nesta área de trabalho e visualizar uma nota selecionada. Instale-o e verifique se a página abre.', + 'Resultado esperado: um plug-in instalado com uma entrada funcional e uma página de visualização, além de quaisquer etapas de ativação restantes.', + '### Adicionar uma ferramenta', + '> Crie um plugin com uma ferramenta que leia o relatório de teste deste projeto e resuma os testes que falharam. Registre-o e verifique-o com um relatório de amostra.', + 'Resultado esperado: um plugin com uma ferramenta que pode ser chamada e uma chamada de amostra verificada.', + '### Crie meu próprio modo', + '> Crie um modo “Revisão de código” baseado no modo Padrão. Faça com que ele priorize possíveis bugs e teste lacunas, cite caminhos e linhas de arquivos e pergunte antes de modificar arquivos. Salve-o como uma predefinição selecionável.', + 'Resultado esperado: uma predefinição personalizada para novas tarefas. Estas instruções de revisão orientam o agente; as configurações de permissão determinam quais ações ele pode executar.', + ].join('\n\n\n\n'), +} satisfies typeof guideEn diff --git a/packages/client/ui-agent-preset/src/client/index.ts b/packages/client/ui-agent-preset/src/client/index.ts index 0d9d61e621..b9e1363c6d 100644 --- a/packages/client/ui-agent-preset/src/client/index.ts +++ b/packages/client/ui-agent-preset/src/client/index.ts @@ -42,7 +42,7 @@ import { AgentPresetSection } from './AgentPresetSection.tsx' import type { AgentPresetSectionInjected } from './AgentPresetSection.tsx' import { AgentPresetSeatController, type AgentPresetStage } from './seat-store.ts' import { AgentPresetSectionController } from './section-store.ts' -import { en, ja, zh, type AgentPresetSettingsKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type AgentPresetSettingsKey } from './locales.ts' import { AGENT_PRESET_SETTINGS_NS, AgentPresetSettingsController } from './settings-store.ts' import { analyticsMode } from './analytics.ts' @@ -128,7 +128,7 @@ export function apply(ctx: ClientContext): void { return binding === undefined ? undefined : seatFor(binding) } - ctx.effect(() => ctx.locale.register('settings.agentPreset', { zh, en, ja }), 'ui-agent-preset: settings row dictionaries') + ctx.effect(() => ctx.locale.register('settings.agentPreset', { zh, en, ja, es, fr, de, ru, pt }), 'ui-agent-preset: settings row dictionaries') ctx.effect(() => { let requested = 0 diff --git a/packages/client/ui-agent-preset/src/client/locales.ts b/packages/client/ui-agent-preset/src/client/locales.ts index 00fce156ee..f5a3c8ed67 100644 --- a/packages/client/ui-agent-preset/src/client/locales.ts +++ b/packages/client/ui-agent-preset/src/client/locales.ts @@ -1,6 +1,6 @@ /** Locale bundles for the agent-preset hero chip, header label, and management section. */ -import { guideEn, guideJa, guideZh, type PresetGuideKey } from './guide-locales.ts' +import { guideEn, guideJa, guideEs, guideFr, guideDe, guideRu, guidePt, guideZh, type PresetGuideKey } from './guide-locales.ts' /** Locale keys these surfaces render. */ export type AgentPresetSettingsKey = @@ -158,3 +158,228 @@ export const ja = { 'createPluginUnavailable': '一時的に利用できません。このメニューを開き直して再試行してください', 'createPluginMissing': 'この構成にはクリエイターモードが含まれていません', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + ...guideEs, + builtInGroup: 'Incorporado', customGroup: 'personalizado', + sectionIntro: 'Elija las herramientas del agente y cómo funciona. Utilice el modo Estándar para las tareas diarias o el modo Creador para agregar capacidades a DSH.', + + seatHint: 'Elija el agente preestablecido para su nueva tarea', + headerHint: 'El agente preestablecido elegido cuando comenzó esta tarea', + nav: 'Ajustes preestablecidos del agente', + + setDefault: 'Establecer como nueva tarea predeterminada', + view: 'Ver configuración', + + presetStandardName: 'Modo estándar', + presetStandardDescription: + 'Trabaje con código, archivos e información. Adecuado para la mayoría de las tareas, con búsqueda, edición, comandos de terminal y otras herramientas disponibles según sea necesario.', + presetPtcName: 'Modo PTC', + presetPtcDescription: + 'Incluye todas las capacidades del modo Estándar. Más adecuado para tareas que llaman a herramientas en lotes y luego filtran, organizan, deduplican, cuentan o resumen los resultados.', + presetMinimalName: 'modo mínimo', + presetMinimalDescription: + 'El agente utiliza un terminal y lectura de imágenes, con compactación de contexto. Útil para probar y comparar su rendimiento básico.', + presetCordisName: 'Modo creador', + presetCordisDescription: + 'Personaliza DSH a través de la conversación. Deje que el agente escriba complementos que agreguen funciones o interfaz de usuario, o combine herramientas e indicaciones para crear su propio modo.', + + inUse: 'Nueva tarea predeterminada', + + noDescription: 'Sin descripción.', + brokenBadge: 'No se pudo cargar', + + switchRefused: 'No se pudo cambiar a {name}: {reason}', + standardUnavailable: 'El modo estándar no está disponible. Restáuralo o elige otro modo disponible.', + + close: 'Cerrar', + + creatorDraft: 'Deja que el agente me ayude a crear un preset', + createPlugin: 'Deje que el agente cree un complemento', + createPluginDescription: 'Ingrese al modo Creador y cree su propio complemento DSH', + createPluginChecking: 'Comprobando si el modo Creador está disponible', + createPluginUnavailable: 'Temporalmente no disponible. Vuelve a abrir este menú para volver a intentarlo.', + createPluginMissing: 'El modo creador no está incluido en esta configuración.', + +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + ...guideFr, + builtInGroup: 'Intégré', customGroup: 'Personnalisé', + sectionIntro: 'Choisissez les outils de l’agent et son fonctionnement. Utilisez le mode Standard pour les tâches quotidiennes ou le mode Créateur pour ajouter des fonctionnalités à DSH.', + + seatHint: "Choisissez le préréglage d'agent pour votre nouvelle tâche", + headerHint: "Le préréglage d'agent choisi au démarrage de cette tâche", + nav: "Préréglages d'agent", + + setDefault: 'Définir comme nouvelle tâche par défaut', + view: 'Afficher la configuration', + + presetStandardName: 'Mode standard', + presetStandardDescription: + 'Travaillez avec du code, des fichiers et des informations. Convient à la plupart des tâches, avec recherche, édition, commandes de terminal et autres outils disponibles selon les besoins.', + presetPtcName: 'Mode PTC', + presetPtcDescription: + 'Inclut toutes les fonctionnalités du mode Standard. Mieux adapté aux tâches qui appellent des outils par lots, puis filtrent, organisent, dédupliquent, comptent ou résument les résultats.', + presetMinimalName: 'Mode minimal', + presetMinimalDescription: + "L'agent utilise un terminal et une lecture d'image, avec compactage du contexte. Utile pour tester et comparer ses performances de base.", + presetCordisName: 'Mode Créateur', + presetCordisDescription: + "Personnalisez DSH via la conversation. Laissez l'agent écrire des plugins qui ajoutent des fonctionnalités ou une interface utilisateur, ou combinez des outils et des invites pour créer votre propre mode.", + + inUse: 'Nouvelle tâche par défaut', + + noDescription: 'Aucune description.', + brokenBadge: 'Échec du chargement', + + switchRefused: 'Impossible de passer à {name} : {reason}', + standardUnavailable: "Le mode standard n'est pas disponible. Restaurez-le ou choisissez un autre mode disponible.", + + close: 'Fermer', + + creatorDraft: "Laissez l'agent m'aider à créer un préréglage", + createPlugin: "Laissez l'agent créer un plugin", + createPluginDescription: 'Entrez en mode Créateur et créez votre propre plugin DSH', + createPluginChecking: 'Vérifier si le mode Créateur est disponible', + createPluginUnavailable: 'Temporairement indisponible. Rouvrez ce menu pour réessayer', + createPluginMissing: "Le mode Créateur n'est pas inclus dans cette configuration", + +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + ...guideDe, + builtInGroup: 'Eingebaut', customGroup: 'Benutzerdefiniert', + sectionIntro: 'Wählen Sie die Tools des Agenten und deren Funktionsweise aus. Verwenden Sie den Standardmodus für alltägliche Aufgaben oder den Creator-Modus, um DSH Funktionen hinzuzufügen.', + + seatHint: 'Wählen Sie die Agentenvoreinstellung für Ihre neue Aufgabe', + headerHint: 'Die Agentenvoreinstellung, die beim Start dieser Aufgabe ausgewählt wurde', + nav: 'Agentenvoreinstellungen', + + setDefault: 'Als Standard für neue Aufgabe festlegen', + view: 'Konfiguration anzeigen', + + presetStandardName: 'Standardmodus', + presetStandardDescription: + 'Arbeiten Sie mit Code, Dateien und Informationen. Geeignet für die meisten Aufgaben, da bei Bedarf Suche, Bearbeitung, Terminalbefehle und andere Tools verfügbar sind.', + presetPtcName: 'PTC-Modus', + presetPtcDescription: + 'Beinhaltet alle Funktionen des Standardmodus. Besser geeignet für Aufgaben, bei denen Tools stapelweise aufgerufen und die Ergebnisse dann gefiltert, organisiert, dedupliziert, gezählt oder zusammengefasst werden.', + presetMinimalName: 'Minimalmodus', + presetMinimalDescription: + 'Der Agent verwendet ein Terminal und das Lesen von Bildern mit Kontextkomprimierung. Nützlich zum Testen und Vergleichen der Grundleistung.', + presetCordisName: 'Erstellermodus', + presetCordisDescription: + 'Passen Sie DSH durch Gespräche an. Lassen Sie den Agenten Plugins schreiben, die Funktionen oder Benutzeroberflächen hinzufügen, oder kombinieren Sie Tools und Eingabeaufforderungen, um Ihren eigenen Modus zu erstellen.', + + inUse: 'Neuer Aufgabenstandard', + + noDescription: 'Keine Beschreibung.', + brokenBadge: 'Laden fehlgeschlagen', + + switchRefused: 'Konnte nicht zu {name} wechseln: {reason}', + standardUnavailable: 'Der Standardmodus ist nicht verfügbar. Stellen Sie es wieder her oder wählen Sie einen anderen verfügbaren Modus.', + + close: 'Schließen', + + creatorDraft: 'Lassen Sie sich vom Agenten beim Erstellen einer Voreinstellung helfen', + createPlugin: 'Lassen Sie den Agenten ein Plugin erstellen', + createPluginDescription: 'Wechseln Sie in den Erstellermodus und erstellen Sie Ihr eigenes DSH-Plugin', + createPluginChecking: 'Prüfen, ob der Creator-Modus verfügbar ist', + createPluginUnavailable: 'Vorübergehend nicht verfügbar. Öffnen Sie dieses Menü erneut, um es erneut zu versuchen', + createPluginMissing: 'Der Creator-Modus ist in dieser Konfiguration nicht enthalten', + +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + ...guideRu, + builtInGroup: 'Встроенный', customGroup: 'Пользовательский', + sectionIntro: 'Выбирайте инструменты агента и то, как они работают. Используйте стандартный режим для повседневных задач или режим Creator, чтобы расширить возможности DSH.', + + seatHint: 'Выберите предустановку агента для вашей новой задачи', + headerHint: 'Предварительная настройка агента, выбранная при запуске этой задачи', + nav: 'Настройки агента', + + setDefault: 'Установить в качестве новой задачи по умолчанию', + view: 'Посмотреть конфигурацию', + + presetStandardName: 'Стандартный режим', + presetStandardDescription: + 'Работайте с кодом, файлами и информацией. Подходит для большинства задач, с поиском, редактированием, командами терминала и другими инструментами, доступными по мере необходимости.', + presetPtcName: 'Режим PTC', + presetPtcDescription: + 'Включает все возможности стандартного режима. Лучше подходит для задач, которые вызывают инструменты в пакетном режиме, а затем фильтруют, организуют, дедуплицируют, подсчитывают или суммируют результаты.', + presetMinimalName: 'Минимальный режим', + presetMinimalDescription: + 'Агент использует терминал и чтение изображений со сжатием контекста. Полезно для тестирования и сравнения его базовой производительности.', + presetCordisName: 'Режим автора', + presetCordisDescription: + 'Настройте DSH через разговор. Позвольте агенту писать плагины, которые добавляют функции или пользовательский интерфейс, или комбинируйте инструменты и подсказки для создания собственного режима.', + + inUse: 'Новая задача по умолчанию', + + noDescription: 'Нет описания.', + brokenBadge: 'Не удалось загрузить', + + switchRefused: 'Не удалось переключиться на {name}: {reason}', + standardUnavailable: 'Стандартный режим недоступен. Восстановите его или выберите другой доступный режим.', + + close: 'Закрыть', + + creatorDraft: 'Позвольте агенту помочь мне создать пресет', + createPlugin: 'Позвольте агенту создать плагин', + createPluginDescription: 'Войдите в режим Creator и создайте свой собственный плагин DSH.', + createPluginChecking: 'Проверка доступности режима Creator', + createPluginUnavailable: 'Временно недоступен. Откройте это меню еще раз, чтобы повторить попытку.', + createPluginMissing: 'Режим Creator не включен в эту конфигурацию.', + +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + ...guidePt, + builtInGroup: 'Integrado', customGroup: 'Personalizado', + sectionIntro: 'Escolha as ferramentas do agente e como funciona. Use o modo Padrão para tarefas diárias ou o modo Criador para adicionar recursos ao DSH.', + + seatHint: 'Escolha a predefinição de agente para sua nova tarefa', + headerHint: 'A predefinição do agente escolhida quando esta tarefa foi iniciada', + nav: 'Predefinições de agente', + + setDefault: 'Definir como nova tarefa padrão', + view: 'Ver configuração', + + presetStandardName: 'Modo padrão', + presetStandardDescription: + 'Trabalhe com código, arquivos e informações. Adequado para a maioria das tarefas, com pesquisa, edição, comandos de terminal e outras ferramentas disponíveis conforme necessário.', + presetPtcName: 'modo PTC', + presetPtcDescription: + 'Inclui todos os recursos do modo Padrão. Mais adequado para tarefas que chamam ferramentas em lotes e depois filtram, organizam, desduplicam, contam ou resumem os resultados.', + presetMinimalName: 'Modo mínimo', + presetMinimalDescription: + 'O agente utiliza terminal e leitura de imagens, com compactação de contexto. Útil para testar e comparar seu desempenho básico.', + presetCordisName: 'Modo criador', + presetCordisDescription: + 'Personalize DSH por meio de conversa. Deixe o agente escrever plug-ins que adicionem recursos ou UI, ou combine ferramentas e prompts para criar seu próprio modo.', + + inUse: 'Novo padrão de tarefa', + + noDescription: 'Nenhuma descrição.', + brokenBadge: 'Falha ao carregar', + + switchRefused: 'Não foi possível mudar para {name}: {reason}', + standardUnavailable: 'O modo padrão não está disponível. Restaure-o ou escolha outro modo disponível.', + + close: 'Fechar', + + creatorDraft: 'Deixe o agente me ajudar a criar uma predefinição', + createPlugin: 'Deixe o agente criar um plugin', + createPluginDescription: 'Entre no modo Criador e crie seu próprio plug-in DSH', + createPluginChecking: 'Verificando se o modo Criador está disponível', + createPluginUnavailable: 'Temporariamente indisponível. Reabra este menu para tentar novamente', + createPluginMissing: 'O modo criador não está incluído nesta configuração', + +} satisfies Record diff --git a/packages/client/ui-approval/locale/de.json b/packages/client/ui-approval/locale/de.json new file mode 100644 index 0000000000..ee89d86fe1 --- /dev/null +++ b/packages/client/ui-approval/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zur Betriebsgenehmigung", + "description": "Zeigt Aktionen an, deren Genehmigung noch aussteht, und führt Benutzerauswahlen zu laufenden Aufgaben zurück." + } +} diff --git a/packages/client/ui-approval/locale/es.json b/packages/client/ui-approval/locale/es.json new file mode 100644 index 0000000000..115786cd1c --- /dev/null +++ b/packages/client/ui-approval/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de aprobación de operación", + "description": "Muestra acciones pendientes de aprobación y devuelve las selecciones del usuario a las tareas en ejecución." + } +} diff --git a/packages/client/ui-approval/locale/fr.json b/packages/client/ui-approval/locale/fr.json new file mode 100644 index 0000000000..69768c0bab --- /dev/null +++ b/packages/client/ui-approval/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface d'approbation des opérations", + "description": "Affiche les actions en attente d'approbation et renvoie les sélections de l'utilisateur aux tâches en cours d'exécution." + } +} diff --git a/packages/client/ui-approval/locale/pt.json b/packages/client/ui-approval/locale/pt.json new file mode 100644 index 0000000000..7fe00b6570 --- /dev/null +++ b/packages/client/ui-approval/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de aprovação de operação", + "description": "Exibe ações com aprovação pendente e retorna as seleções do usuário para tarefas em execução." + } +} diff --git a/packages/client/ui-approval/locale/ru.json b/packages/client/ui-approval/locale/ru.json new file mode 100644 index 0000000000..b656b6b2d5 --- /dev/null +++ b/packages/client/ui-approval/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс одобрения операций", + "description": "Отображает действия, ожидающие утверждения, и возвращает выбранные пользователем действия к выполняемым задачам." + } +} diff --git a/packages/client/ui-approval/src/client/index.ts b/packages/client/ui-approval/src/client/index.ts index a667d507e9..4073a85aa2 100644 --- a/packages/client/ui-approval/src/client/index.ts +++ b/packages/client/ui-approval/src/client/index.ts @@ -11,7 +11,7 @@ import type { ShortcutCommandId } from '@deepseek-ai/dsh-client-shortcuts/client import { ApprovalCard } from './ApprovalCard.tsx' import { ApprovalPanel } from './ApprovalPanel.tsx' import { PendingApproval } from './contract/slots.ts' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' export type { ApprovalCardInput, @@ -77,7 +77,7 @@ async function answerApproval( * @param ctx - Client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-approval: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-approval: dictionaries') ctx.inject(['shortcuts'], (scope) => { const t = ctx.locale.bind(NS) scope.effect(() => scope.shortcuts.registerFixed({ diff --git a/packages/client/ui-approval/src/client/locales.ts b/packages/client/ui-approval/src/client/locales.ts index c8e540fd49..99408815d9 100644 --- a/packages/client/ui-approval/src/client/locales.ts +++ b/packages/client/ui-approval/src/client/locales.ts @@ -35,3 +35,58 @@ export const ja = { 'allowOnce': '1 回だけ許可', 'allowAlways': '常に許可', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + waiting: 'Esperando aprobación', + 'detail.aria': 'Detalles de aprobación', + escalation: 'La herramienta {toolName} solicita ejecución privilegiada', + reject: 'Rechazar', + allow: 'Permitir', + allowOnce: 'permitir una vez', + allowAlways: 'Permitir siempre', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + waiting: "En attente d'approbation", + 'detail.aria': "Détails de l'approbation", + escalation: "L'outil {toolName} demande une exécution privilégiée", + reject: 'Rejeter', + allow: 'Autoriser', + allowOnce: 'Autoriser une fois', + allowAlways: 'Toujours autoriser', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + waiting: 'Warten auf Genehmigung', + 'detail.aria': 'Einzelheiten zur Genehmigung', + escalation: 'Tool {toolName} fordert privilegierte Ausführung an', + reject: 'Ablehnen', + allow: 'Erlauben', + allowOnce: 'Einmal zulassen', + allowAlways: 'Immer zulassen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + waiting: 'Ожидание одобрения', + 'detail.aria': 'Детали утверждения', + escalation: 'Инструмент {toolName} запрашивает привилегированное выполнение', + reject: 'Отклонить', + allow: 'Разрешить', + allowOnce: 'Разрешить один раз', + allowAlways: 'Всегда разрешать', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + waiting: 'Aguardando aprovação', + 'detail.aria': 'Detalhes de aprovação', + escalation: 'A ferramenta {toolName} solicita execução privilegiada', + reject: 'Rejeitar', + allow: 'Permitir', + allowOnce: 'Permitir uma vez', + allowAlways: 'Sempre permitir', +} satisfies Record diff --git a/packages/client/ui-attachment/locale/de.json b/packages/client/ui-attachment/locale/de.json new file mode 100644 index 0000000000..07c79593b0 --- /dev/null +++ b/packages/client/ui-attachment/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Anhangsanzeige", + "description": "Zeigen Sie Anhänge und Bilder in Nachrichteneingaben, Konversationen und Ausführungsprotokollen an." + } +} diff --git a/packages/client/ui-attachment/locale/es.json b/packages/client/ui-attachment/locale/es.json new file mode 100644 index 0000000000..635cff5f3d --- /dev/null +++ b/packages/client/ui-attachment/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Visualización de archivos adjuntos", + "description": "Muestre archivos adjuntos e imágenes en la entrada de mensajes, conversaciones y registros de ejecución." + } +} diff --git a/packages/client/ui-attachment/locale/fr.json b/packages/client/ui-attachment/locale/fr.json new file mode 100644 index 0000000000..c89a8bf6f9 --- /dev/null +++ b/packages/client/ui-attachment/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Affichage des pièces jointes", + "description": "Affichez les pièces jointes et les images dans la saisie des messages, les conversations et les journaux d'exécution." + } +} diff --git a/packages/client/ui-attachment/locale/pt.json b/packages/client/ui-attachment/locale/pt.json new file mode 100644 index 0000000000..6295a88f6d --- /dev/null +++ b/packages/client/ui-attachment/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Exibição de anexo", + "description": "Exiba anexos e imagens em entradas de mensagens, conversas e registros de execução." + } +} diff --git a/packages/client/ui-attachment/locale/ru.json b/packages/client/ui-attachment/locale/ru.json new file mode 100644 index 0000000000..8f81ace343 --- /dev/null +++ b/packages/client/ui-attachment/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Отображение вложений", + "description": "Отображение вложений и изображений при вводе сообщений, разговорах и журналах запуска." + } +} diff --git a/packages/client/ui-brand-official/locale/de.json b/packages/client/ui-brand-official/locale/de.json new file mode 100644 index 0000000000..953f8dbf7e --- /dev/null +++ b/packages/client/ui-brand-official/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek Markendarstellung", + "description": "Zeigen Sie offizielle Markenelemente an Orten wie der App-Seitenleiste an." + } +} diff --git a/packages/client/ui-brand-official/locale/es.json b/packages/client/ui-brand-official/locale/es.json new file mode 100644 index 0000000000..108c571dbe --- /dev/null +++ b/packages/client/ui-brand-official/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek Visualización de marca", + "description": "Muestre elementos oficiales de la marca en lugares como la barra lateral de la aplicación." + } +} diff --git a/packages/client/ui-brand-official/locale/fr.json b/packages/client/ui-brand-official/locale/fr.json new file mode 100644 index 0000000000..5a47875c99 --- /dev/null +++ b/packages/client/ui-brand-official/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek Affichage de la marque", + "description": "Affichez les éléments officiels de la marque dans des endroits tels que la barre latérale de l'application." + } +} diff --git a/packages/client/ui-brand-official/locale/pt.json b/packages/client/ui-brand-official/locale/pt.json new file mode 100644 index 0000000000..2b06cd6c7a --- /dev/null +++ b/packages/client/ui-brand-official/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek Exibição da marca", + "description": "Exiba elementos oficiais da marca em locais como a barra lateral do aplicativo." + } +} diff --git a/packages/client/ui-brand-official/locale/ru.json b/packages/client/ui-brand-official/locale/ru.json new file mode 100644 index 0000000000..e3e9d5cead --- /dev/null +++ b/packages/client/ui-brand-official/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek Дисплей бренда", + "description": "Отображайте официальные элементы бренда в таких местах, как боковая панель приложения." + } +} diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index d3e5dc6506..7cc337dbb2 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/README.i18n.yaml @@ -24,8 +24,8 @@ en: 41dcd70e14ebe9cc zh: ededead572b59037 /deepseek-ai-dsh-client-ui-chat/turn-token-usage: - en: 059eef868161efe4 - zh: 52ecc8e682d4c630 + en: cfdfe3d99d3f4e09 + zh: 9330bad3d3f7a689 /deepseek-ai-dsh-client-ui-chat/completed-turn-footer: en: 382821ca3ffa60dd zh: 9840ffa07c13082a diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index b5dc85a0f9..85b3e65c8d 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -70,7 +70,7 @@ A completed Turn shows an expandable usage row only when the loaded window inclu Settings → General → Performance & usage appears after Send behavior while busy and before Session Log upload, and stores `ui-chat.performanceUsage` as `detailed` (default) or `compact`. Compact shows only available output speed and cache-hit percentage beneath the composer, without interactive statistic dialogs or per-Turn usage. Detailed exposes session statistics and per-Turn token usage. Neither mode shows elapsed time in the completed-turn footer. The preference changes presentation only; accounting and Session events remain intact. -The composer statistics are two `conversation.composer.dock` list entries: `activity` (order 0, turn and step counts and output speed, with LLM time, tool time, and TTFT) and `usage` (order 1, token total and cache hit). The controls follow the shared [statistics-control radius](../../../docs/ui-radius.md#radius-scale). Another plugin replaces one control by registering the same id and the same `order`; the dock orders rows by `order`, while a lower `priority` picks the replacement: a dynamic plugin receives one automatically, and a statically composed plugin passes it explicitly. A new id adds a control. Each control carries its id in `data-composer-stat`. An open statistics dialog closes on Escape or on a pointerdown or click outside it, so activating another control by pointer or keyboard swaps dialogs instead of stacking them. +The composer statistics are two `conversation.composer.dock` list entries: `activity` (order 0, turn and step counts and output speed, with LLM time, tool time, and TTFT) and `usage` (order 1, token total and cache hit). The controls follow the shared [statistics-control radius](../../../docs/ui-radius.md#radius-scale). Another plugin replaces one control by registering the same id and the same `order`; the dock orders rows by `order`, while a lower `priority` picks the replacement: a dynamic plugin receives one automatically, and a statically composed plugin passes it explicitly. A new id adds a control. Each control carries its id in `data-composer-stat`. An open statistics dialog closes on Escape or on a pointerdown or click outside it, so activating another control by pointer or keyboard swaps dialogs instead of stacking them. When an ellipsis cuts off a reading, a tooltip shows the full reading. A dialog control shows it on hover or keyboard focus, but not while its dialog is open; a reading without a dialog shows it on hover, click, or tap. The session statistics dialog heads its rows with the turn and step counts. Explicit selections update every consumer immediately; accepted Host settings reconcile the live value, so a refused or pending write keeps the selected mode until the Host answers. diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index 06d1d924b2..486da291e6 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -70,7 +70,7 @@ Assistant 尝试结束且没有可见消息时,Chat 隐藏已发布的 Node, 设置 → 通用设置 → 性能与用量位于繁忙时的发送行为之后、Session Log 上传之前,将 `ui-chat.performanceUsage` 保存为 `detailed`(默认)或 `compact`。简洁模式仅在输入框下方显示可用的输出速度和缓存命中率,不显示统计交互卡片或每轮用量。详细模式提供会话统计和每轮 token 用量。两种模式的已完成轮次页脚均不显示耗时。该偏好仅影响展示,记账和 Session 事件保持完整。 -输入框统计信息由两个 `conversation.composer.dock` 列表条目组成:`activity`(order 0,轮次数、步骤数与输出速度,以及 LLM 耗时、工具耗时与 TTFT)和 `usage`(order 1,token 总量与缓存命中)。这些控件遵循公共的[统计控件圆角规则](../../../docs/ui-radius.zh.md#radius-scale)。其他插件以相同的 id 和相同的 `order` 注册即可替换某个控件;dock 按 `order` 排列各行,更低的 `priority` 决定由替换者渲染:动态插件会自动获得,静态组合的插件需显式传入。以新 id 注册则新增控件。每个控件在 `data-composer-stat` 中携带其 id。按下 Escape,或在已打开的对话框之外发生 pointerdown 或点击时,该对话框会关闭,因此用指针或键盘激活另一个控件会切换对话框,而不会叠加两个。 +输入框统计信息由两个 `conversation.composer.dock` 列表条目组成:`activity`(order 0,轮次数、步骤数与输出速度,以及 LLM 耗时、工具耗时与 TTFT)和 `usage`(order 1,token 总量与缓存命中)。这些控件遵循公共的[统计控件圆角规则](../../../docs/ui-radius.zh.md#radius-scale)。其他插件以相同的 id 和相同的 `order` 注册即可替换某个控件;dock 按 `order` 排列各行,更低的 `priority` 决定由替换者渲染:动态插件会自动获得,静态组合的插件需显式传入。以新 id 注册则新增控件。每个控件在 `data-composer-stat` 中携带其 id。按下 Escape,或在已打开的对话框之外发生 pointerdown 或点击时,该对话框会关闭,因此用指针或键盘激活另一个控件会切换对话框,而不会叠加两个。读数被省略号截断时,提示气泡会显示完整读数。带对话框的控件在悬停或键盘聚焦时显示,对话框打开期间不显示;没有对话框的读数在悬停、点击或轻触时显示。会话统计对话框的标题行显示轮次数与步骤数。 明确选择会立即更新所有使用方;Host 接受的设置会校正实时值,因此写入被拒绝或尚未应答时保持所选模式,直到 Host 应答。 diff --git a/packages/client/ui-chat/locale/de.json b/packages/client/ui-chat/locale/de.json new file mode 100644 index 0000000000..15ae984c6b --- /dev/null +++ b/packages/client/ui-chat/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Anzeige von Chat-Inhalten", + "description": "Rendern Sie Chat-Nachrichten, Tool-Inhalte und zugehörige Details." + } +} diff --git a/packages/client/ui-chat/locale/es.json b/packages/client/ui-chat/locale/es.json new file mode 100644 index 0000000000..eb088700ac --- /dev/null +++ b/packages/client/ui-chat/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Visualización del contenido del chat", + "description": "Representa mensajes de chat, contenido de herramientas y detalles relacionados." + } +} diff --git a/packages/client/ui-chat/locale/fr.json b/packages/client/ui-chat/locale/fr.json new file mode 100644 index 0000000000..95efa01a16 --- /dev/null +++ b/packages/client/ui-chat/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Affichage du contenu du chat", + "description": "Rendre les messages de discussion, le contenu de l'outil et les détails associés." + } +} diff --git a/packages/client/ui-chat/locale/pt.json b/packages/client/ui-chat/locale/pt.json new file mode 100644 index 0000000000..0a5efa2be2 --- /dev/null +++ b/packages/client/ui-chat/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Exibição de conteúdo de bate-papo", + "description": "Renderize mensagens de bate-papo, conteúdo de ferramentas e detalhes relacionados." + } +} diff --git a/packages/client/ui-chat/locale/ru.json b/packages/client/ui-chat/locale/ru.json new file mode 100644 index 0000000000..ffb6e15666 --- /dev/null +++ b/packages/client/ui-chat/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Отображение содержимого чата", + "description": "Отображение сообщений чата, содержимого инструментов и связанных с ними деталей." + } +} diff --git a/packages/client/ui-chat/src/client/apply.ts b/packages/client/ui-chat/src/client/apply.ts index fcbc44d9be..fbabf95903 100644 --- a/packages/client/ui-chat/src/client/apply.ts +++ b/packages/client/ui-chat/src/client/apply.ts @@ -37,7 +37,7 @@ import { UserContent } from './chat/UserContent.tsx' import { ActivityPill, UsagePill } from './chat/StatsPills.tsx' import { registerConversationNodes } from './conversation-nodes/register.ts' import { QuotaNoticeHost } from './chat/QuotaNoticeHost.tsx' -import { en, ja, NS, zh } from './locale.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh } from './locale.ts' import { TranscriptViewRow, type TranscriptViewRowInjected } from './settings/TranscriptViewRow.tsx' import { createChatStore } from './stores.ts' import { TranscriptViewPolicy } from './transcript-view.ts' @@ -134,7 +134,7 @@ export function apply(ctx: Context): void { resolve: binding => ({ hooks: { chat: chatSource(binding) } }), }) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-chat: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-chat: dictionaries') const t = ctx.locale.bind(NS) const chatStore = createChatStore() const chatScrollPositions = new Map() diff --git a/packages/client/ui-chat/src/client/chat/StatsPills.tsx b/packages/client/ui-chat/src/client/chat/StatsPills.tsx index 838effb768..7108c44719 100644 --- a/packages/client/ui-chat/src/client/chat/StatsPills.tsx +++ b/packages/client/ui-chat/src/client/chat/StatsPills.tsx @@ -6,7 +6,7 @@ import { memo, useMemo, type ReactNode } from 'react' import { createPortal } from 'react-dom' -import { IconDatabaseOutlineRegular, IconGaugeOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconDatabaseOutlineRegular, IconGaugeOutlineRegular, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' import type { UseProjection } from '@deepseek-ai/dsh-api-session-controller/client' import type { InjectFace, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots' // Type-only: merges the sessionStats key into SessionProjectionMap for useProjection. @@ -136,21 +136,27 @@ interface PillContent { stat: StatId icon: ReactNode label: ReactNode + /** The full reading as plain text: the accessible name and the tooltip shown while the label is cut off. */ + text: string } function exactCount(value: number, t: Translate): string { return t('message.turnUsage.count', { count: formatExactTokens(value, t) }) } -function joined(first: string, second: string | null): ReactNode { - if (second === null) return first - return ( - <> - {first} - · - {second} - - ) +/** One or two reading segments, visually joined by a separator glyph. */ +function reading(first: string, second: string | null): Pick { + if (second === null) return { label: first, text: first } + return { + label: ( + <> + {first} + · + {second} + + ), + text: `${first} · ${second}`, + } } // Every figure rides the durable sessionStats projection, so paging and @@ -169,14 +175,20 @@ function decodeSpeed(stats: WindowStats, t: Translate): string { }) } +/** The full reading above a pill, only while an ellipsis cuts its label; the delay matches the dock's context meter. */ +const FULL_READING_TOOLTIP = { side: 'top', portal: true, delayMs: 200, onlyWhenClipped: true } as const + /** A static reading: used when the pill has no dialog rows or the mode is Compact. */ -function PlainPill({ stat, icon, label }: PillContent) { +function PlainPill({ stat, icon, label, text }: PillContent) { return ( - - {icon} - {label} - + {/* No dialog carries a static reading, so a tap also pins the full text. */} + + + {icon} + {label} + + ) } @@ -186,8 +198,7 @@ function PlainPill({ stat, icon, label }: PillContent) { * state; useStatDialog closes it on Escape or an outside pointerdown or click, * so at most one dialog is open across the dock. */ -function DialogPill({ stat, icon, label, ariaLabel, title, titleValue, children }: PillContent & { - ariaLabel: string +function DialogPill({ stat, icon, label, text, title, titleValue, children }: PillContent & { title: string titleValue?: string children: ReactNode @@ -195,17 +206,20 @@ function DialogPill({ stat, icon, label, ariaLabel, title, titleValue, children const { open, setOpen, rootRef, panelRef, pos } = useStatDialog() return ( - + {/* The open dialog already shows every figure; a tooltip would cover it. */} + + + {open && createPortal(
0 ? decodeSpeed(stats, t) : null const icon = - if (mode === 'compact') return speed === null ? null : + if (mode === 'compact') return speed === null ? null : if (stats.steps === 0) return null const counts = t('stats.counts', { turns: stats.turns, steps: stats.steps }) - const content: PillContent = { stat: 'activity', icon, label: joined(counts, speed) } + const content: PillContent = { stat: 'activity', icon, ...reading(counts, speed) } // A window without one timed figure has no dialog rows to show, so the pill // stays a plain reading instead of a button opening an empty dialog. if (stats.llmMs <= 0 && stats.toolMs <= 0 && stats.ttftSteps <= 0 && speed === null) return return ( - + // The counts head the dialog, so a cut-off reading stays complete once the dialog opens. +
{stats.llmMs > 0 && ( <> @@ -289,7 +304,7 @@ export const UsagePill = memo(function UsagePill({ useProjection, usePerformance const cacheHitText = cacheHit !== null ? t('stats.cacheHit', { percent: cacheHit }) : null const icon = if (mode === 'compact') { - return cacheHitText === null ? null : + return cacheHitText === null ? null : } // Same aggregate as the Turn pill's totalTokens: every prompt-side billing bucket plus output. const total = billedInputTokens(usage) + usage.outputTokens @@ -298,8 +313,7 @@ export const UsagePill = memo(function UsagePill({ useProjection, usePerformance diff --git a/packages/client/ui-chat/src/client/locale.ts b/packages/client/ui-chat/src/client/locale.ts index c5ffa1e87c..6f0e6eda52 100644 --- a/packages/client/ui-chat/src/client/locale.ts +++ b/packages/client/ui-chat/src/client/locale.ts @@ -602,3 +602,998 @@ export const ja = { 'clock.md': '{m}/{d}', 'clock.ymd': '{y}-{m}-{d}', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'message.stepProcess.thinking': 'Analizando la solicitud', + 'message.stepProcess.read': 'Leyendo archivos', + 'message.stepProcess.readImage': 'Leyendo imágenes', + 'message.stepProcess.write': 'Escribir archivos', + 'message.stepProcess.search': 'código de búsqueda', + 'message.stepProcess.edit': 'Editar archivos', + 'message.stepProcess.commands': 'Ejecutar comandos', + 'message.stepProcess.code': 'Código en ejecución', + 'message.stepProcess.webSearch': 'buscando en la web', + 'message.stepProcess.webFetch': 'Visitar páginas web', + 'message.stepProcess.subagents': 'Subagentes coordinadores', + 'message.stepProcess.plan': 'Actualizando el plan', + 'message.stepProcess.questions': 'Esperando tu acción', + 'message.stepProcess.tools': 'herramientas de llamada', + 'message.stepProcess.prepare.read': 'Preparándose para leer archivos', + 'message.stepProcess.prepare.readImage': 'Preparándose para leer imágenes', + 'message.stepProcess.prepare.write': 'Preparándose para escribir archivos', + 'message.stepProcess.prepare.search': 'Preparándose para buscar el código', + 'message.stepProcess.prepare.edit': 'Preparándose para editar archivos', + 'message.stepProcess.prepare.commands': 'Preparándose para ejecutar comandos', + 'message.stepProcess.prepare.code': 'Preparándose para ejecutar código', + 'message.stepProcess.prepare.webSearch': 'Preparándose para buscar en la web', + 'message.stepProcess.prepare.webFetch': 'Preparándose para visitar páginas web', + 'message.stepProcess.prepare.subagents': 'Preparándose para coordinar subagentes', + 'message.stepProcess.prepare.plan': 'Preparándose para actualizar el plan', + 'message.stepProcess.prepare.questions': 'Preparando preguntas', + 'message.stepProcess.prepare.tools': 'Preparando llamadas a herramientas', + 'message.stepProcess.done.thinking': 'Análisis completado', + 'message.stepProcess.done.read': 'leer archivos', + 'message.stepProcess.done.readImage': 'Leer imágenes', + 'message.stepProcess.done.write': 'Escribió archivos', + 'message.stepProcess.done.search': 'código buscado', + 'message.stepProcess.done.edit': 'Archivos editados', + 'message.stepProcess.done.commands': 'Comandos ejecutados', + 'message.stepProcess.done.code': 'código ejecutado', + 'message.stepProcess.done.webSearch': 'Busqué en la web', + 'message.stepProcess.done.webFetch': 'Paginas web visitadas', + 'message.stepProcess.done.subagents': 'Subagentes coordinados', + 'message.stepProcess.done.plan': 'Actualizado el plan', + 'message.stepProcess.done.questions': 'preguntas hechas', + 'message.stepProcess.done.tools': 'herramientas llamadas', + 'message.stepProcess.joinTwo': '{first} y {second}', + 'message.stepProcess.comma': ', ', + 'message.stepProcess.sharedPrefix': '', + 'message.stepProcess.more': '{title}, etc.', + 'message.trigger.request': 'Ejecución solicitada', + 'message.trigger.goal': 'Objetivo continuo', + 'message.trigger.agent': 'Mensaje de tarea recibido', + 'message.trigger.team': 'Mensaje del equipo recibido', + 'message.trigger.subagent': 'Estado de subtarea actualizado', + 'message.trigger.github': 'Evento GitHub recibido', + 'message.trigger.webhook': 'Evento externo recibido', + 'message.trigger.schedule': 'Tarea de automatización', + 'message.trigger.job': 'Tarea en segundo plano actualizada', + 'message.trigger.plugin': 'Estado del complemento actualizado', + 'message.trigger.explanation': 'Esta notificación desencadenó esta respuesta.', + 'message.turnProcess.worked': 'Completado', + 'message.turnProcess.took': 'Completado en ', + 'message.turnProcess.failed': 'Fallido', + 'view.chat': 'Chat', + 'image.open': 'Ver imagen completa', + 'image.loading': 'Cargando imagen…', + 'image.failed': 'Vista previa de la imagen no disponible', + 'image.dialog': 'Vista previa de la imagen', + 'image.close': 'Cerrar vista previa de la imagen', + 'number.groupSeparator': ',', + 'duration.compactSeconds': '{seconds}s', + 'duration.compactMinutes': '{minutes}m{seconds}s', + 'duration.milliseconds': '{milliseconds}ms', + 'stats.counts': '{turns} turnos, {steps} pasos', + 'stats.cacheHit': 'Aciertos de caché: {percent}%', + 'stats.dialog.title': 'Estadísticas de sesión', + 'stats.dialog.usageTitle': 'Uso de tokens', + 'stats.dialog.llmTime': 'tiempo de maestría', + 'stats.dialog.toolTime': 'tiempo de herramienta', + 'stats.dialog.ttft': 'Tiempo promedio hasta el primer token (TTFT)', + 'stats.dialog.speed': 'Fichas por segundo (TPS)', + 'chat.loadingHistory': 'Cargando historial…', + 'chat.loadError': 'No se pudo cargar el historial: {message} ({code})', + 'chat.loadOlder': 'Cargar antes', + 'chat.toBottom': 'Volver al fondo', + 'chat.deepDiving': 'Razonamiento profundo', + 'chat.deepDivingFor': 'Razonando a fondo durante {duration} ···', + 'chat.turnNavigation.label': 'Navegación por turnos', + 'chat.turnNavigation.jump': 'Ir al turno {turn}', + 'chat.turnNavigation.jumpLoad': 'Cargar e ir al turno {turn}', + 'chat.turnNavigation.turn': 'Turno {turn}', + 'settings.performance.title': 'Rendimiento y uso', + 'settings.performance.description': 'Elija cuánta información de rendimiento y uso desea mostrar', + 'settings.performance.compact': 'Compacto', + 'settings.performance.detailed': 'Detallado', + 'settings.links.title': 'Abrir enlaces de chat en', + 'settings.links.description': 'Elija dónde abrir enlaces web', + 'settings.links.sidebar': 'Barra lateral en la aplicación', + 'settings.links.newTab': 'Navegador predeterminado', + 'settings.transcript.title': 'Detalles del trabajo', + 'settings.transcript.description': 'Elija cuántos detalles mostrar para las llamadas de herramientas', + 'settings.collapse.title': 'Cuándo contraer los detalles del trabajo', + 'settings.collapse.description': 'Elija cuándo contraer automáticamente los detalles del trabajo', + 'settings.collapse.completion': 'Al finalizar', + 'settings.collapse.nextInput': 'En el siguiente mensaje', + 'settings.foldedReplies.title': 'Respuestas después del plegado', + 'settings.foldedReplies.description': 'Elija qué respuestas del asistente permanecen visibles cuando los detalles del trabajo colapsan', + 'settings.foldedReplies.all': 'Todas las respuestas', + 'settings.foldedReplies.final': 'Solo respuesta final', + 'settings.transcript.compact': 'Compacto', + 'settings.transcript.standard': 'Estándar', + 'settings.transcript.detailed': 'Detallado', + 'settings.transcript.verbose': 'Detallado', + 'fileOpen.title': 'No se pudo abrir el archivo', + 'fileOpen.unknown': 'No se pudo abrir este archivo', + 'message.extraBlock': 'Bloque de contenido adicional', + 'message.systemPrompt': 'Aviso del sistema', + 'message.systemPromptUpdate': 'Actualización del mensaje del sistema', + 'message.toolAdded': 'Herramienta agregada: {name}', + 'message.toolRemoved': 'Herramienta eliminada: {name}', + 'message.toolsAdded': 'Añadido: {names}', + 'message.toolsAddedCount': '{count} añadido', + 'message.toolsChanged': '{added} agregado, {removed} eliminado', + 'message.toolsRemoved': 'Eliminado: {names}', + 'message.toolsRemovedCount': '{count} eliminado', + 'message.toolsUpdated': 'Herramientas actualizadas', + 'message.contextInjection': 'inyección de contexto', + 'message.contextRecall': 'Recuerdo de sesión', + 'message.referenceSummary': 'Sesión referenciada · {labels}', + 'message.referenceSeparator': ', ', + 'message.context.instructions.loaded': 'cargado', + 'message.context.instructions.added': 'añadido', + 'message.context.instructions.updated': 'actualizado', + 'message.context.instructions.removed': 'eliminado', + 'message.context.catalog.replaced': 'Catálogo de repuestos', + 'message.context.catalog.more': '… {count} más', + 'message.context.snapshot.supersedes': 'Reemplaza instantáneas anteriores', + 'message.context.relay.from': 'De la sesión {session}', + 'message.context.recall.counts': '{retained} mantenido · {omitted} omitido', + 'message.context.recall.truncated': 'truncado', + 'message.compaction': 'Contexto compactado', + 'message.compaction.running': 'Contexto de compactación…', + 'message.compaction.completed': 'Elementos del historial {items} compactados (~tokens {tokens})', + 'message.compaction.expand': 'Ver resumen de compactación', + 'message.compaction.unavailable': 'Resumen de compactación no disponible', + 'message.compaction.commandTitle': 'compacto', + 'message.think': 'pensar', + 'message.unknownSurface': 'Evento de superficie desconocido: {type}', + 'message.unknownBlock': 'Bloque de contenido desconocido', + 'message.turnProcess.toolCalls.one': 'Llamada a la herramienta {count}', + 'message.turnProcess.toolCalls.other': 'Llamadas a herramientas {count}', + 'message.turnProcess.messages.one': 'Mensaje {count}', + 'message.turnProcess.messages.other': 'Mensajes {count}', + 'message.turnProcess.subagents.one': 'Subagente {count}', + 'message.turnProcess.subagents.other': 'Subagentes {count}', + 'message.turnProcess.thoughtForAWhile': 'Razonó durante un tiempo', + 'message.turnProcess.separator': ' · ', + 'message.stopped': 'Detenido', + 'message.branch': 'Pasar a una nueva conversación', + 'message.branchUnavailable': 'Disponible solo en el último mensaje de un turno completado', + 'message.retry.active': 'Reintentando solicitud de modelo', + 'message.retry.cancelled': 'Reintento de solicitud de modelo cancelado', + 'message.retry.started': 'Solicitud de modelo reintentada', + 'message.retry.scheduled': 'Esperando para volver a intentar la solicitud de modelo', + 'message.retry.status': '{label} ({retry}/{maximum}) · {seconds}s', + 'message.retry.delay': 'Retraso de reintento: ', + 'message.retry.failure': 'Razón del fracaso: ', + 'message.failure.auth': 'La clave API no es válida', + 'message.taskStopped': 'Tarea detenida', + 'message.failure.accountSignedOut': 'Se detuvo porque cerró sesión en DeepSeek.', + 'message.failure.interrupted': 'El proceso DSH salió antes de que terminara este turno (por ejemplo, se detuvo, se estrelló o se agotó el tiempo de espera mientras se apagaba), por lo que el turno se cerró cuando se reinició DSH. Las llamadas a herramientas inacabadas tienen resultados desconocidos.', + 'message.failure.accountSignInRequired': 'Inicie sesión en DeepSeek y asegúrese de que el destino de la solicitud admita la autenticación de la cuenta.', + 'message.failure.quota': 'Solicitud de cupo agotado.', + 'message.turnError': 'Este turno falló', + 'message.maxTokens': 'Se alcanzó el límite del token de salida', + 'message.maxTokens.hint': 'La respuesta fue cortada; La salida anterior se conserva en la conversación. Envíe "continuar" para permitir que se reanude el modelo.', + 'message.tokensPerSecond': '{tps} tok/s', + 'message.turnUsage.title': 'Uso del turno', + 'message.turnUsage.consumed': 'Uso {total}', + 'message.turnUsage.model': 'Proveedor / modelo', + 'message.turnUsage.cacheHit': 'Aciertos de caché', + 'message.turnUsage.input': 'Entrada sin caché', + 'message.turnUsage.cacheRead': 'Entrada en caché', + 'message.turnUsage.cacheWrite': 'escritura en caché', + 'message.turnUsage.output': 'Salida', + 'message.turnUsage.reasoning': ' (razonamiento {tokens})', + 'message.turnUsage.count': 'Tok {count}', + 'duration.secondUnit': 's', + 'duration.minuteUnit': 'm ', + 'duration.hourUnit': 'h ', + 'command.running': 'Corriendo…', + 'command.failed': 'El comando falló', + 'command.done': 'Completado', + 'command.title': 'Comando', + 'row.running': 'corriendo', + 'row.failed': 'Fallido', + 'json.truncated': '… truncado, caracteres {total} en total', + 'clock.md': '{m}/{d}', + 'clock.ymd': '{y}-{m}-{d}', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'message.stepProcess.thinking': 'Analyser la demande', + 'message.stepProcess.read': 'Lecture de fichiers', + 'message.stepProcess.readImage': 'Lire des images', + 'message.stepProcess.write': 'Écriture de fichiers', + 'message.stepProcess.search': 'Recherche de code', + 'message.stepProcess.edit': 'Modification de fichiers', + 'message.stepProcess.commands': 'Exécuter des commandes', + 'message.stepProcess.code': 'Exécution du code', + 'message.stepProcess.webSearch': 'Recherche sur le Web', + 'message.stepProcess.webFetch': 'Visiter des pages Web', + 'message.stepProcess.subagents': 'Sous-agents de coordination', + 'message.stepProcess.plan': 'Mise à jour du plan', + 'message.stepProcess.questions': 'En attendant votre action', + 'message.stepProcess.tools': "Outils d'appel", + 'message.stepProcess.prepare.read': 'Préparation à la lecture de fichiers', + 'message.stepProcess.prepare.readImage': 'Se préparer à lire des images', + 'message.stepProcess.prepare.write': "Préparation à l'écriture de fichiers", + 'message.stepProcess.prepare.search': 'Préparation à la recherche de code', + 'message.stepProcess.prepare.edit': 'Préparation à la modification de fichiers', + 'message.stepProcess.prepare.commands': "Préparation à l'exécution de commandes", + 'message.stepProcess.prepare.code': "Préparation à l'exécution du code", + 'message.stepProcess.prepare.webSearch': 'Se préparer à effectuer une recherche sur le Web', + 'message.stepProcess.prepare.webFetch': 'Se préparer à visiter des pages Web', + 'message.stepProcess.prepare.subagents': 'Se préparer à coordonner les sous-agents', + 'message.stepProcess.prepare.plan': 'Préparation de la mise à jour du plan', + 'message.stepProcess.prepare.questions': 'Préparation des questions', + 'message.stepProcess.prepare.tools': "Préparation des appels d'outils", + 'message.stepProcess.done.thinking': 'Analyse terminée', + 'message.stepProcess.done.read': 'Lire des fichiers', + 'message.stepProcess.done.readImage': 'Lire des images', + 'message.stepProcess.done.write': 'A écrit des fichiers', + 'message.stepProcess.done.search': 'Code recherché', + 'message.stepProcess.done.edit': 'Fichiers modifiés', + 'message.stepProcess.done.commands': 'Commandes exécutées', + 'message.stepProcess.done.code': 'Code exécuté', + 'message.stepProcess.done.webSearch': "J'ai cherché sur le Web", + 'message.stepProcess.done.webFetch': 'Pages Web visitées', + 'message.stepProcess.done.subagents': 'Sous-agents coordonnés', + 'message.stepProcess.done.plan': 'Plan mis à jour', + 'message.stepProcess.done.questions': 'Questions posées', + 'message.stepProcess.done.tools': 'Outils appelés', + 'message.stepProcess.joinTwo': '{first} et {second}', + 'message.stepProcess.comma': ', ', + 'message.stepProcess.sharedPrefix': '', + 'message.stepProcess.more': '{title}, etc.', + 'message.trigger.request': 'Exécution demandée', + 'message.trigger.goal': 'Objectif continu', + 'message.trigger.agent': 'Message de tâche reçu', + 'message.trigger.team': "Message d'équipe reçu", + 'message.trigger.subagent': 'Statut de la sous-tâche mis à jour', + 'message.trigger.github': 'Événement GitHub reçu', + 'message.trigger.webhook': 'Événement externe reçu', + 'message.trigger.schedule': "Tâche d'automatisation", + 'message.trigger.job': 'Tâche en arrière-plan mise à jour', + 'message.trigger.plugin': 'Statut du plugin mis à jour', + 'message.trigger.explanation': 'Cette notification a déclenché cette réponse.', + 'message.turnProcess.worked': 'Terminé', + 'message.turnProcess.took': 'Terminé en ', + 'message.turnProcess.failed': 'Échec', + 'view.chat': 'Discussion', + 'image.open': "Voir l'image complète", + 'image.loading': "Chargement de l'image…", + 'image.failed': "Aperçu de l'image indisponible", + 'image.dialog': "Aperçu de l'image", + 'image.close': "Fermer l'aperçu de l'image", + 'number.groupSeparator': ',', + 'duration.compactSeconds': '{seconds}s', + 'duration.compactMinutes': '{minutes}m{seconds}s', + 'duration.milliseconds': '{milliseconds}ms', + 'stats.counts': '{turns} tours, {steps} étapes', + 'stats.cacheHit': 'Taux de réussite du cache : {percent} %', + 'stats.dialog.title': 'Statistiques de session', + 'stats.dialog.usageTitle': 'Utilisation des jetons', + 'stats.dialog.llmTime': 'Temps LLM', + 'stats.dialog.toolTime': "Temps d'outil", + 'stats.dialog.ttft': "Temps moyen jusqu'au premier jeton (TTFT)", + 'stats.dialog.speed': 'Jetons par seconde (TPS)', + 'chat.loadingHistory': "Chargement de l'historique…", + 'chat.loadError': "Échec du chargement de l'historique : {message} ({code})", + 'chat.loadOlder': 'Charger plus tôt', + 'chat.toBottom': 'Retour en bas', + 'chat.deepDiving': 'Réflexion approfondie', + 'chat.deepDivingFor': 'Réflexion approfondie depuis {duration} ···', + 'chat.turnNavigation.label': 'Navigation entre les tours', + 'chat.turnNavigation.jump': 'Aller au tour {turn}', + 'chat.turnNavigation.jumpLoad': 'Charger et aller au tour {turn}', + 'chat.turnNavigation.turn': 'Tour {turn}', + 'settings.performance.title': 'Performances et utilisation', + 'settings.performance.description': "Choisissez la quantité d'informations sur les performances et l'utilisation à afficher", + 'settings.performance.compact': 'Compacte', + 'settings.performance.detailed': 'Détaillé', + 'settings.links.title': 'Ouvrir les liens de discussion dans', + 'settings.links.description': 'Choisissez où ouvrir les liens Web', + 'settings.links.sidebar': "Barre latérale dans l'application", + 'settings.links.newTab': 'Navigateur par défaut', + 'settings.transcript.title': 'Détails du travail', + 'settings.transcript.description': "Choisissez le niveau de détails à afficher pour les appels d'outils", + 'settings.collapse.title': 'Quand réduire les détails du travail', + 'settings.collapse.description': 'Choisissez quand réduire automatiquement les détails du travail', + 'settings.collapse.completion': 'À la fin', + 'settings.collapse.nextInput': 'Au prochain message', + 'settings.foldedReplies.title': 'Réponses après le repli', + 'settings.foldedReplies.description': "Choisissez quelles réponses de l'assistant restent visibles lorsque les détails du travail sont réduits", + 'settings.foldedReplies.all': 'Toutes les réponses', + 'settings.foldedReplies.final': 'Réponse finale seulement', + 'settings.transcript.compact': 'Compacte', + 'settings.transcript.standard': 'Norme', + 'settings.transcript.detailed': 'Détaillé', + 'settings.transcript.verbose': 'Verbeux', + 'fileOpen.title': "Impossible d'ouvrir le fichier", + 'fileOpen.unknown': "Impossible d'ouvrir ce fichier", + 'message.extraBlock': 'Bloc de contenu supplémentaire', + 'message.systemPrompt': 'Invite système', + 'message.systemPromptUpdate': "Mise à jour de l'invite du système", + 'message.toolAdded': 'Outil ajouté : {name}', + 'message.toolRemoved': 'Outil supprimé : {name}', + 'message.toolsAdded': 'Ajouté : {names}', + 'message.toolsAddedCount': '{count} ajouté', + 'message.toolsChanged': '{added} ajouté, {removed} supprimé', + 'message.toolsRemoved': 'Supprimé : {names}', + 'message.toolsRemovedCount': '{count} supprimé', + 'message.toolsUpdated': 'Outils mis à jour', + 'message.contextInjection': 'Injection de contexte', + 'message.contextRecall': 'Rappel de session', + 'message.referenceSummary': 'Séance référencée · {labels}', + 'message.referenceSeparator': ', ', + 'message.context.instructions.loaded': 'chargé', + 'message.context.instructions.added': 'ajouté', + 'message.context.instructions.updated': 'mis à jour', + 'message.context.instructions.removed': 'supprimé', + 'message.context.catalog.replaced': 'Catalogue de remplacement', + 'message.context.catalog.more': '… {count} plus', + 'message.context.snapshot.supersedes': 'Remplace les instantanés précédents', + 'message.context.relay.from': 'De la séance {session}', + 'message.context.recall.counts': '{retained} conservé · {omitted} omis', + 'message.context.recall.truncated': 'tronqué', + 'message.compaction': 'Contexte compacté', + 'message.compaction.running': 'Contexte de compactage…', + 'message.compaction.completed': "Éléments d'historique {items} compactés (~jetons {tokens})", + 'message.compaction.expand': 'Afficher le résumé du compactage', + 'message.compaction.unavailable': 'Résumé du compactage indisponible', + 'message.compaction.commandTitle': 'compacte', + 'message.think': 'Pensez', + 'message.unknownSurface': 'Événement de surface inconnu : {type}', + 'message.unknownBlock': 'Bloc de contenu inconnu', + 'message.turnProcess.toolCalls.one': "Appel d'outil {count}", + 'message.turnProcess.toolCalls.other': "Appels d'outil {count}", + 'message.turnProcess.messages.one': 'Message {count}', + 'message.turnProcess.messages.other': 'Messages {count}', + 'message.turnProcess.subagents.one': 'Sous-agent {count}', + 'message.turnProcess.subagents.other': 'Sous-agents {count}', + 'message.turnProcess.thoughtForAWhile': 'Réflexion pendant un moment', + 'message.turnProcess.separator': ' · ', + 'message.stopped': 'Arrêté', + 'message.branch': 'Lancez-vous dans une nouvelle conversation', + 'message.branchUnavailable': "Disponible uniquement sur le dernier message d'un tour terminé", + 'message.retry.active': 'Nouvelle tentative de demande de modèle', + 'message.retry.cancelled': 'Nouvelle tentative de demande de modèle annulée', + 'message.retry.started': 'Demande de modèle réessayée', + 'message.retry.scheduled': 'En attente de réessayer la demande de modèle', + 'message.retry.status': '{label} ({retry}/{maximum}) · {seconds}', + 'message.retry.delay': 'Délai de nouvelle tentative : ', + 'message.retry.failure': "Raison de l'échec : ", + 'message.failure.auth': "La clé API n'est pas valide", + 'message.taskStopped': 'Tâche arrêtée', + 'message.failure.accountSignedOut': 'Arrêté car vous vous êtes déconnecté de DeepSeek.', + 'message.failure.interrupted': "Le processus DSH s'est terminé avant la fin de ce tour (par exemple, il a été arrêté, s'est écrasé ou a expiré lors de l'arrêt), de sorte que le tour a été fermé lorsque DSH a redémarré. Les appels d’outils inachevés ont des résultats inconnus.", + 'message.failure.accountSignInRequired': "Connectez-vous à DeepSeek et assurez-vous que la destination de la demande prend en charge l'authentification du compte.", + 'message.failure.quota': 'Quota de demandes épuisé.', + 'message.turnError': 'Ce tour a échoué', + 'message.maxTokens': 'Limite de jetons de sortie atteinte', + 'message.maxTokens.hint': 'La réponse fut coupée ; la sortie antérieure est conservée dans la conversation. Envoyez « continuer » pour laisser le modèle reprendre.', + 'message.tokensPerSecond': '{tps} jetons/s', + 'message.turnUsage.title': 'Consommation du tour', + 'message.turnUsage.consumed': 'Utilisation {total}', + 'message.turnUsage.model': 'Fournisseur / modèle', + 'message.turnUsage.cacheHit': 'Taux de réussite du cache', + 'message.turnUsage.input': 'Entrée non mise en cache', + 'message.turnUsage.cacheRead': 'Entrée en cache', + 'message.turnUsage.cacheWrite': 'Écriture en cache', + 'message.turnUsage.output': 'Sortie', + 'message.turnUsage.reasoning': ' (raisonnement {tokens})', + 'message.turnUsage.count': 'Jeton {count}', + 'duration.secondUnit': 's', + 'duration.minuteUnit': 'm ', + 'duration.hourUnit': 'h ', + 'command.running': 'Courir…', + 'command.failed': 'La commande a échoué', + 'command.done': 'Terminé', + 'command.title': 'Commande', + 'row.running': 'Courir', + 'row.failed': 'Échec', + 'json.truncated': '… tronqué, nombre total de caractères {total}', + 'clock.md': '{m}/{d}', + 'clock.ymd': '{y}-{m}-{d}', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'message.stepProcess.thinking': 'Analyse der Anfrage', + 'message.stepProcess.read': 'Dateien lesen', + 'message.stepProcess.readImage': 'Bilder lesen', + 'message.stepProcess.write': 'Dateien schreiben', + 'message.stepProcess.search': 'Code wird gesucht', + 'message.stepProcess.edit': 'Bearbeiten von Dateien', + 'message.stepProcess.commands': 'Befehle ausführen', + 'message.stepProcess.code': 'Ausführen von Code', + 'message.stepProcess.webSearch': 'Suche im Internet', + 'message.stepProcess.webFetch': 'Besuch von Webseiten', + 'message.stepProcess.subagents': 'Koordinierende Unteragenten', + 'message.stepProcess.plan': 'Aktualisierung des Plans', + 'message.stepProcess.questions': 'Ich warte auf Ihre Aktion', + 'message.stepProcess.tools': 'Aufrufende Werkzeuge', + 'message.stepProcess.prepare.read': 'Vorbereitung zum Lesen von Dateien', + 'message.stepProcess.prepare.readImage': 'Vorbereitung zum Lesen von Bildern', + 'message.stepProcess.prepare.write': 'Vorbereitung zum Schreiben von Dateien', + 'message.stepProcess.prepare.search': 'Vorbereiten der Codesuche', + 'message.stepProcess.prepare.edit': 'Vorbereitung zum Bearbeiten von Dateien', + 'message.stepProcess.prepare.commands': 'Vorbereitung zum Ausführen von Befehlen', + 'message.stepProcess.prepare.code': 'Vorbereiten, Code auszuführen', + 'message.stepProcess.prepare.webSearch': 'Bereiten Sie sich auf die Suche im Internet vor', + 'message.stepProcess.prepare.webFetch': 'Vorbereitung auf den Besuch von Webseiten', + 'message.stepProcess.prepare.subagents': 'Vorbereitung zur Koordinierung von Subagenten', + 'message.stepProcess.prepare.plan': 'Vorbereitung zur Aktualisierung des Plans', + 'message.stepProcess.prepare.questions': 'Fragen vorbereiten', + 'message.stepProcess.prepare.tools': 'Toolaufrufe vorbereiten', + 'message.stepProcess.done.thinking': 'Analyse abgeschlossen', + 'message.stepProcess.done.read': 'Dateien lesen', + 'message.stepProcess.done.readImage': 'Bilder lesen', + 'message.stepProcess.done.write': 'Dateien geschrieben', + 'message.stepProcess.done.search': 'Gesuchter Code', + 'message.stepProcess.done.edit': 'Bearbeitete Dateien', + 'message.stepProcess.done.commands': 'Befehle ausgeführt', + 'message.stepProcess.done.code': 'Code ausgeführt', + 'message.stepProcess.done.webSearch': 'Habe das Internet durchsucht', + 'message.stepProcess.done.webFetch': 'Besuchte Webseiten', + 'message.stepProcess.done.subagents': 'Koordinierte Unteragenten', + 'message.stepProcess.done.plan': 'Plan aktualisiert', + 'message.stepProcess.done.questions': 'Gestellte Fragen', + 'message.stepProcess.done.tools': 'Werkzeuge genannt', + 'message.stepProcess.joinTwo': '{first} und {second}', + 'message.stepProcess.comma': ', ', + 'message.stepProcess.sharedPrefix': '', + 'message.stepProcess.more': '{title} usw.', + 'message.trigger.request': 'Ausführung beantragt', + 'message.trigger.goal': 'Weiterführendes Ziel', + 'message.trigger.agent': 'Aufgabennachricht erhalten', + 'message.trigger.team': 'Teamnachricht erhalten', + 'message.trigger.subagent': 'Unteraufgabenstatus aktualisiert', + 'message.trigger.github': 'GitHub-Ereignis empfangen', + 'message.trigger.webhook': 'Externes Ereignis empfangen', + 'message.trigger.schedule': 'Automatisierungsaufgabe', + 'message.trigger.job': 'Hintergrundaufgabe aktualisiert', + 'message.trigger.plugin': 'Plugin-Status aktualisiert', + 'message.trigger.explanation': 'Diese Benachrichtigung hat diese Reaktion ausgelöst.', + 'message.turnProcess.worked': 'Abgeschlossen', + 'message.turnProcess.took': 'Fertiggestellt in ', + 'message.turnProcess.failed': 'Fehlgeschlagen', + 'view.chat': 'Chat', + 'image.open': 'Vollständiges Bild anzeigen', + 'image.loading': 'Bild wird geladen…', + 'image.failed': 'Bildvorschau nicht verfügbar', + 'image.dialog': 'Bildvorschau', + 'image.close': 'Bildvorschau schließen', + 'number.groupSeparator': ',', + 'duration.compactSeconds': '{seconds}s', + 'duration.compactMinutes': '{minutes}m{seconds}s', + 'duration.milliseconds': '{milliseconds}ms', + 'stats.counts': '{turns} Runden, {steps} Schritte', + 'stats.cacheHit': 'Cache-Treffer: {percent}%', + 'stats.dialog.title': 'Sitzungsstatistiken', + 'stats.dialog.usageTitle': 'Token-Nutzung', + 'stats.dialog.llmTime': 'LLM-Zeit', + 'stats.dialog.toolTime': 'Werkzeugzeit', + 'stats.dialog.ttft': 'Durchschnittliche Zeit bis zum ersten Token (TTFT)', + 'stats.dialog.speed': 'Token pro Sekunde (TPS)', + 'chat.loadingHistory': 'Verlauf wird geladen…', + 'chat.loadError': 'Fehler beim Laden des Verlaufs: {message} ({code})', + 'chat.loadOlder': 'Früher laden', + 'chat.toBottom': 'Zurück nach unten', + 'chat.deepDiving': 'Gründliches Nachdenken', + 'chat.deepDivingFor': 'Gründliches Nachdenken seit {duration} ···', + 'chat.turnNavigation.label': 'Rundennavigation', + 'chat.turnNavigation.jump': 'Zu Runde {turn} springen', + 'chat.turnNavigation.jumpLoad': 'Runde {turn} laden und öffnen', + 'chat.turnNavigation.turn': 'Runde {turn}', + 'settings.performance.title': 'Leistung und Nutzung', + 'settings.performance.description': 'Wählen Sie aus, wie viele Leistungs- und Nutzungsinformationen angezeigt werden sollen', + 'settings.performance.compact': 'Kompakt', + 'settings.performance.detailed': 'Ausführlich', + 'settings.links.title': 'Öffnen Sie Chat-Links in', + 'settings.links.description': 'Wählen Sie aus, wo Weblinks geöffnet werden sollen', + 'settings.links.sidebar': 'In-App-Seitenleiste', + 'settings.links.newTab': 'Standardbrowser', + 'settings.transcript.title': 'Arbeitsdetails', + 'settings.transcript.description': 'Wählen Sie aus, wie viele Details für Werkzeugaufrufe angezeigt werden sollen', + 'settings.collapse.title': 'Wann Arbeitsdetails minimiert werden sollten', + 'settings.collapse.description': 'Wählen Sie aus, wann Arbeitsdetails automatisch ausgeblendet werden sollen', + 'settings.collapse.completion': 'Nach Fertigstellung', + 'settings.collapse.nextInput': 'Bei der nächsten Nachricht', + 'settings.foldedReplies.title': 'Antworten nach dem Einklappen', + 'settings.foldedReplies.description': 'Wählen Sie aus, welche Assistentenantworten sichtbar bleiben, wenn die Arbeitsdetails ausgeblendet werden', + 'settings.foldedReplies.all': 'Alle Antworten', + 'settings.foldedReplies.final': 'Nur die endgültige Antwort', + 'settings.transcript.compact': 'Kompakt', + 'settings.transcript.standard': 'Standard', + 'settings.transcript.detailed': 'Ausführlich', + 'settings.transcript.verbose': 'Ausführlich', + 'fileOpen.title': 'Datei konnte nicht geöffnet werden', + 'fileOpen.unknown': 'Diese Datei konnte nicht geöffnet werden', + 'message.extraBlock': 'Zusätzlicher Inhaltsblock', + 'message.systemPrompt': 'Systemaufforderung', + 'message.systemPromptUpdate': 'Aktualisierung der Systemaufforderung', + 'message.toolAdded': 'Tool hinzugefügt: {name}', + 'message.toolRemoved': 'Entferntes Werkzeug: {name}', + 'message.toolsAdded': 'Hinzugefügt: {names}', + 'message.toolsAddedCount': '{count} hinzugefügt', + 'message.toolsChanged': '{added} hinzugefügt, {removed} entfernt', + 'message.toolsRemoved': 'Entfernt: {names}', + 'message.toolsRemovedCount': '{count} entfernt', + 'message.toolsUpdated': 'Werkzeuge aktualisiert', + 'message.contextInjection': 'Kontextinjektion', + 'message.contextRecall': 'Sitzungsrückruf', + 'message.referenceSummary': 'Referenzierte Sitzung · {labels}', + 'message.referenceSeparator': ', ', + 'message.context.instructions.loaded': 'geladen', + 'message.context.instructions.added': 'hinzugefügt', + 'message.context.instructions.updated': 'aktualisiert', + 'message.context.instructions.removed': 'entfernt', + 'message.context.catalog.replaced': 'Ersatzkatalog', + 'message.context.catalog.more': '… {count} mehr', + 'message.context.snapshot.supersedes': 'Ersetzt frühere Schnappschüsse', + 'message.context.relay.from': 'Aus der Sitzung {session}', + 'message.context.recall.counts': '{retained} beibehalten · {omitted} weggelassen', + 'message.context.recall.truncated': 'abgeschnitten', + 'message.compaction': 'Kontext verdichtet', + 'message.compaction.running': 'Kontext verdichten…', + 'message.compaction.completed': 'Komprimierte {items}-Verlaufselemente (~{tokens}-Tokens)', + 'message.compaction.expand': 'Komprimierungszusammenfassung anzeigen', + 'message.compaction.unavailable': 'Komprimierungszusammenfassung nicht verfügbar', + 'message.compaction.commandTitle': 'kompakt', + 'message.think': 'Denken Sie nach', + 'message.unknownSurface': 'Unbekanntes Oberflächenereignis: {type}', + 'message.unknownBlock': 'Unbekannter Inhaltsblock', + 'message.turnProcess.toolCalls.one': 'Werkzeugaufruf {count}', + 'message.turnProcess.toolCalls.other': '{count} Toolaufrufe', + 'message.turnProcess.messages.one': '{count}-Nachricht', + 'message.turnProcess.messages.other': '{count}-Nachrichten', + 'message.turnProcess.subagents.one': '{count}-Subagent', + 'message.turnProcess.subagents.other': '{count}-Subagenten', + 'message.turnProcess.thoughtForAWhile': 'Eine Weile nachgedacht', + 'message.turnProcess.separator': ' · ', + 'message.stopped': 'Angehalten', + 'message.branch': 'Verzweigen Sie in ein neues Gespräch', + 'message.branchUnavailable': 'Nur verfügbar bei der letzten Nachricht einer abgeschlossenen Runde', + 'message.retry.active': 'Modellanforderung wird erneut versucht', + 'message.retry.cancelled': 'Wiederholungsversuch der Modellanforderung abgebrochen', + 'message.retry.started': 'Modellanfrage erneut versucht', + 'message.retry.scheduled': 'Warten auf erneuten Versuch der Modellanforderung', + 'message.retry.status': '{label} ({retry}/{maximum}) · {seconds}s', + 'message.retry.delay': 'Wiederholungsverzögerung: ', + 'message.retry.failure': 'Fehlergrund: ', + 'message.failure.auth': 'Der API-Schlüssel ist ungültig', + 'message.taskStopped': 'Aufgabe gestoppt', + 'message.failure.accountSignedOut': 'Gestoppt, weil Sie sich von DeepSeek abgemeldet haben.', + 'message.failure.interrupted': 'Der DSH-Prozess wurde beendet, bevor dieser Zug beendet war (z. B. wurde er beendet, stürzte ab oder es kam zu einer Zeitüberschreitung beim Herunterfahren), sodass der Zug geschlossen wurde, als DSH neu gestartet wurde. Unvollendete Werkzeugaufrufe haben unbekannte Ergebnisse.', + 'message.failure.accountSignInRequired': 'Melden Sie sich bei DeepSeek an und stellen Sie sicher, dass das Anforderungsziel die Kontoauthentifizierung unterstützt.', + 'message.failure.quota': 'Anforderungskontingent ausgeschöpft.', + 'message.turnError': 'Diese Runde ist fehlgeschlagen', + 'message.maxTokens': 'Ausgabe-Token-Limit erreicht', + 'message.maxTokens.hint': 'Die Antwort wurde abgeschnitten; Frühere Ausgaben bleiben in der Konversation erhalten. Senden Sie „Weiter“, um das Modell fortzusetzen.', + 'message.tokensPerSecond': '{tps} tok/s', + 'message.turnUsage.title': 'Rundenverbrauch', + 'message.turnUsage.consumed': 'Verwendung {total}', + 'message.turnUsage.model': 'Anbieter / Modell', + 'message.turnUsage.cacheHit': 'Cache-Treffer', + 'message.turnUsage.input': 'Nicht zwischengespeicherte Eingabe', + 'message.turnUsage.cacheRead': 'Zwischengespeicherte Eingabe', + 'message.turnUsage.cacheWrite': 'Cache-Schreiben', + 'message.turnUsage.output': 'Ausgabe', + 'message.turnUsage.reasoning': ' ({tokens}-Argumentation)', + 'message.turnUsage.count': '{count}-Token', + 'duration.secondUnit': 's', + 'duration.minuteUnit': 'm ', + 'duration.hourUnit': 'h ', + 'command.running': 'Laufen…', + 'command.failed': 'Der Befehl ist fehlgeschlagen', + 'command.done': 'Abgeschlossen', + 'command.title': 'Befehl', + 'row.running': 'Laufen', + 'row.failed': 'Fehlgeschlagen', + 'json.truncated': '… gekürzt, {total} Zeichen insgesamt', + 'clock.md': '{m}/{d}', + 'clock.ymd': '{y}-{m}-{d}', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'message.stepProcess.thinking': 'Анализ запроса', + 'message.stepProcess.read': 'Чтение файлов', + 'message.stepProcess.readImage': 'Чтение изображений', + 'message.stepProcess.write': 'Запись файлов', + 'message.stepProcess.search': 'Поиск кода', + 'message.stepProcess.edit': 'Редактирование файлов', + 'message.stepProcess.commands': 'Запуск команд', + 'message.stepProcess.code': 'Запуск кода', + 'message.stepProcess.webSearch': 'Поиск в Интернете', + 'message.stepProcess.webFetch': 'Посещение веб-страниц', + 'message.stepProcess.subagents': 'Координирующие субагенты', + 'message.stepProcess.plan': 'Обновление плана', + 'message.stepProcess.questions': 'Жду ваших действий', + 'message.stepProcess.tools': 'Вызов инструментов', + 'message.stepProcess.prepare.read': 'Подготовка к чтению файлов', + 'message.stepProcess.prepare.readImage': 'Подготовка к чтению изображений', + 'message.stepProcess.prepare.write': 'Подготовка к записи файлов', + 'message.stepProcess.prepare.search': 'Подготовка к поиску кода', + 'message.stepProcess.prepare.edit': 'Подготовка к редактированию файлов', + 'message.stepProcess.prepare.commands': 'Подготовка к выполнению команд', + 'message.stepProcess.prepare.code': 'Подготовка к запуску кода', + 'message.stepProcess.prepare.webSearch': 'Подготовка к поиску в Интернете', + 'message.stepProcess.prepare.webFetch': 'Подготовка к посещению веб-страниц', + 'message.stepProcess.prepare.subagents': 'Подготовка к координации субагентов', + 'message.stepProcess.prepare.plan': 'Подготовка к обновлению плана', + 'message.stepProcess.prepare.questions': 'Подготовка вопросов', + 'message.stepProcess.prepare.tools': 'Подготовка вызовов инструментов', + 'message.stepProcess.done.thinking': 'Анализ завершен', + 'message.stepProcess.done.read': 'Чтение файлов', + 'message.stepProcess.done.readImage': 'Чтение изображений', + 'message.stepProcess.done.write': 'Написал файлы', + 'message.stepProcess.done.search': 'Искомый код', + 'message.stepProcess.done.edit': 'Отредактированные файлы', + 'message.stepProcess.done.commands': 'Выполнение команд', + 'message.stepProcess.done.code': 'Выполненный код', + 'message.stepProcess.done.webSearch': 'Искал в Интернете', + 'message.stepProcess.done.webFetch': 'Посещенные веб-страницы', + 'message.stepProcess.done.subagents': 'Координированные субагенты', + 'message.stepProcess.done.plan': 'Обновлен план', + 'message.stepProcess.done.questions': 'Задаваемые вопросы', + 'message.stepProcess.done.tools': 'Вызываемые инструменты', + 'message.stepProcess.joinTwo': '{first} и {second}', + 'message.stepProcess.comma': ', ', + 'message.stepProcess.sharedPrefix': '', + 'message.stepProcess.more': '{title} и т. д.', + 'message.trigger.request': 'Запрошено выполнение', + 'message.trigger.goal': 'Постоянная цель', + 'message.trigger.agent': 'Сообщение о задаче получено', + 'message.trigger.team': 'Сообщение команды получено', + 'message.trigger.subagent': 'Статус подзадачи обновлен', + 'message.trigger.github': 'Событие GitHub получено', + 'message.trigger.webhook': 'Получено внешнее событие', + 'message.trigger.schedule': 'Задача автоматизации', + 'message.trigger.job': 'Фоновая задача обновлена.', + 'message.trigger.plugin': 'Статус плагина обновлен', + 'message.trigger.explanation': 'Это уведомление вызвало такой ответ.', + 'message.turnProcess.worked': 'Завершено', + 'message.turnProcess.took': 'Завершено в ', + 'message.turnProcess.failed': 'Не удалось', + 'view.chat': 'Чат', + 'image.open': 'Посмотреть полное изображение', + 'image.loading': 'Загрузка изображения…', + 'image.failed': 'Предварительный просмотр изображения недоступен.', + 'image.dialog': 'Предварительный просмотр изображения', + 'image.close': 'Закрыть предварительный просмотр изображения', + 'number.groupSeparator': ',', + 'duration.compactSeconds': '{seconds}s', + 'duration.compactMinutes': '{minutes}m{seconds}s', + 'duration.milliseconds': '{milliseconds}ms', + 'stats.counts': 'Ходов: {turns}, шагов: {steps}', + 'stats.cacheHit': 'Попадание в кэш: {percent}%', + 'stats.dialog.title': 'Статистика сеансов', + 'stats.dialog.usageTitle': 'Использование токена', + 'stats.dialog.llmTime': 'время магистратуры', + 'stats.dialog.toolTime': 'Время работы инструмента', + 'stats.dialog.ttft': 'Среднее время до первого токена (TTFT)', + 'stats.dialog.speed': 'Токенов в секунду (TPS)', + 'chat.loadingHistory': 'Загрузка истории…', + 'chat.loadError': 'Не удалось загрузить историю: {message} ({code})', + 'chat.loadOlder': 'Загрузить раньше', + 'chat.toBottom': 'Вернуться к низу', + 'chat.deepDiving': 'Глубокое размышление', + 'chat.deepDivingFor': 'Глубокое размышление в течение {duration} ···', + 'chat.turnNavigation.label': 'Навигация по ходам', + 'chat.turnNavigation.jump': 'Перейти к ходу {turn}', + 'chat.turnNavigation.jumpLoad': 'Загрузить ход {turn} и перейти к нему', + 'chat.turnNavigation.turn': 'Ход {turn}', + 'settings.performance.title': 'Производительность и использование', + 'settings.performance.description': 'Выберите, какой объем информации о производительности и использовании показывать', + 'settings.performance.compact': 'Компактный', + 'settings.performance.detailed': 'Подробный', + 'settings.links.title': 'Открыть ссылки на чат в', + 'settings.links.description': 'Выберите, где открывать веб-ссылки', + 'settings.links.sidebar': 'Боковая панель в приложении', + 'settings.links.newTab': 'Браузер по умолчанию', + 'settings.transcript.title': 'Детали работы', + 'settings.transcript.description': 'Выберите, насколько подробно следует отображать вызовы инструментов.', + 'settings.collapse.title': 'Когда сворачивать детали работы', + 'settings.collapse.description': 'Выберите, когда автоматически сворачивать сведения о работе', + 'settings.collapse.completion': 'По завершении', + 'settings.collapse.nextInput': 'В следующем сообщении', + 'settings.foldedReplies.title': 'Ответы после сворачивания', + 'settings.foldedReplies.description': 'Выберите, какие ответы помощника останутся видимыми, когда сведения о работе будут свернуты.', + 'settings.foldedReplies.all': 'Все ответы', + 'settings.foldedReplies.final': 'Только окончательный ответ', + 'settings.transcript.compact': 'Компактный', + 'settings.transcript.standard': 'Стандартный', + 'settings.transcript.detailed': 'Подробный', + 'settings.transcript.verbose': 'Многословный', + 'fileOpen.title': 'Не удалось открыть файл', + 'fileOpen.unknown': 'Не удалось открыть этот файл', + 'message.extraBlock': 'Дополнительный блок контента', + 'message.systemPrompt': 'Системная подсказка', + 'message.systemPromptUpdate': 'Обновление системного запроса', + 'message.toolAdded': 'Добавлен инструмент: {name}.', + 'message.toolRemoved': 'Инструмент удален: {name}.', + 'message.toolsAdded': 'Добавлено: {names}', + 'message.toolsAddedCount': '{count} добавлен', + 'message.toolsChanged': '{added} добавлен, {removed} удален.', + 'message.toolsRemoved': 'Удалено: {names}.', + 'message.toolsRemovedCount': '{count} удален.', + 'message.toolsUpdated': 'Инструменты обновлены', + 'message.contextInjection': 'Внедрение контекста', + 'message.contextRecall': 'Отзыв сеанса', + 'message.referenceSummary': 'Ссылка на сессию · {labels}', + 'message.referenceSeparator': ', ', + 'message.context.instructions.loaded': 'загружен', + 'message.context.instructions.added': 'добавлено', + 'message.context.instructions.updated': 'обновлено', + 'message.context.instructions.removed': 'удалено', + 'message.context.catalog.replaced': 'Каталог замены', + 'message.context.catalog.more': '… {count} ещё', + 'message.context.snapshot.supersedes': 'Заменяет предыдущие снимки', + 'message.context.relay.from': 'Из сеанса {session}', + 'message.context.recall.counts': '{retained} сохранен · {omitted} опущен', + 'message.context.recall.truncated': 'усеченный', + 'message.compaction': 'Контекст сжат', + 'message.compaction.running': 'Сжатие контекста…', + 'message.compaction.completed': 'Сжатые элементы истории {items} (токены ~{tokens})', + 'message.compaction.expand': 'Посмотреть сводку по уплотнению', + 'message.compaction.unavailable': 'Сводка по уплотнению недоступна.', + 'message.compaction.commandTitle': 'компактный', + 'message.think': 'Подумайте', + 'message.unknownSurface': 'Неизвестное событие на поверхности: {type}.', + 'message.unknownBlock': 'Неизвестный блок контента', + 'message.turnProcess.toolCalls.one': '{count} вызов инструмента', + 'message.turnProcess.toolCalls.other': '{count} вызовов инструментов', + 'message.turnProcess.messages.one': 'Сообщение {count}', + 'message.turnProcess.messages.other': 'Сообщения {count}', + 'message.turnProcess.subagents.one': 'Субагент {count}', + 'message.turnProcess.subagents.other': 'Субагенты {count}', + 'message.turnProcess.thoughtForAWhile': 'Размышлял некоторое время', + 'message.turnProcess.separator': ' · ', + 'message.stopped': 'Остановлено', + 'message.branch': 'Перейти к новому разговору', + 'message.branchUnavailable': 'Доступно только в последнем сообщении о завершенном ходу.', + 'message.retry.active': 'Повторный запрос модели', + 'message.retry.cancelled': 'Повтор запроса модели отменен.', + 'message.retry.started': 'Повторный запрос модели', + 'message.retry.scheduled': 'Ожидание повторного запроса модели', + 'message.retry.status': '{label} ({retry}/{maximum}) · {seconds}s', + 'message.retry.delay': 'Задержка повтора: ', + 'message.retry.failure': 'Причина неисправности: ', + 'message.failure.auth': 'Ключ API недействителен.', + 'message.taskStopped': 'Задача остановлена', + 'message.failure.accountSignedOut': 'Остановлено, поскольку вы вышли из DeepSeek.', + 'message.failure.interrupted': 'Процесс DSH завершился до завершения этого хода (например, был завершен, произошел сбой или истекло время ожидания при завершении работы), поэтому ход был закрыт при перезапуске DSH. Незавершенные вызовы инструментов имеют неизвестные результаты.', + 'message.failure.accountSignInRequired': 'Войдите в DeepSeek и убедитесь, что пункт назначения запроса поддерживает аутентификацию учетной записи.', + 'message.failure.quota': 'Квота запросов исчерпана.', + 'message.turnError': 'Этот ход завершился ошибкой', + 'message.maxTokens': 'Достигнут предел выходных токенов', + 'message.maxTokens.hint': 'Ответ был прерван; более ранний вывод сохраняется в разговоре. Отправьте «продолжить», чтобы модель возобновила работу.', + 'message.tokensPerSecond': '{tps} ток/с', + 'message.turnUsage.title': 'Использование ресурсов за ход', + 'message.turnUsage.consumed': 'Использование {total}', + 'message.turnUsage.model': 'Поставщик/модель', + 'message.turnUsage.cacheHit': 'Попадание в кэш', + 'message.turnUsage.input': 'Некэшированный ввод', + 'message.turnUsage.cacheRead': 'Кэшированный ввод', + 'message.turnUsage.cacheWrite': 'Запись в кэш', + 'message.turnUsage.output': 'Выход', + 'message.turnUsage.reasoning': ' (рассуждение {tokens})', + 'message.turnUsage.count': '{count} ток', + 'duration.secondUnit': 'с', + 'duration.minuteUnit': 'м ', + 'duration.hourUnit': 'ч ', + 'command.running': 'Бег…', + 'command.failed': 'Команда не выполнена', + 'command.done': 'Завершено', + 'command.title': 'Команда', + 'row.running': 'Бег', + 'row.failed': 'Не удалось', + 'json.truncated': '… усечено, всего символов {total}', + 'clock.md': '{m}/{d}', + 'clock.ymd': '{y}-{m}-{d}', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'message.stepProcess.thinking': 'Analisando a solicitação', + 'message.stepProcess.read': 'Lendo arquivos', + 'message.stepProcess.readImage': 'Lendo imagens', + 'message.stepProcess.write': 'Gravando arquivos', + 'message.stepProcess.search': 'Pesquisando código', + 'message.stepProcess.edit': 'Editando arquivos', + 'message.stepProcess.commands': 'Executando comandos', + 'message.stepProcess.code': 'Executando código', + 'message.stepProcess.webSearch': 'Pesquisando na web', + 'message.stepProcess.webFetch': 'Visitando páginas da web', + 'message.stepProcess.subagents': 'Coordenando subagentes', + 'message.stepProcess.plan': 'Atualizando o plano', + 'message.stepProcess.questions': 'Esperando por sua ação', + 'message.stepProcess.tools': 'Ferramentas de chamada', + 'message.stepProcess.prepare.read': 'Preparando-se para ler arquivos', + 'message.stepProcess.prepare.readImage': 'Preparando-se para ler imagens', + 'message.stepProcess.prepare.write': 'Preparando-se para gravar arquivos', + 'message.stepProcess.prepare.search': 'Preparando-se para pesquisar o código', + 'message.stepProcess.prepare.edit': 'Preparando-se para editar arquivos', + 'message.stepProcess.prepare.commands': 'Preparando para executar comandos', + 'message.stepProcess.prepare.code': 'Preparando-se para executar o código', + 'message.stepProcess.prepare.webSearch': 'Preparando-se para pesquisar na web', + 'message.stepProcess.prepare.webFetch': 'Preparando-se para visitar páginas da web', + 'message.stepProcess.prepare.subagents': 'Preparando-se para coordenar subagentes', + 'message.stepProcess.prepare.plan': 'Preparando-se para atualizar o plano', + 'message.stepProcess.prepare.questions': 'Preparando perguntas', + 'message.stepProcess.prepare.tools': 'Preparando chamadas de ferramenta', + 'message.stepProcess.done.thinking': 'Análise concluída', + 'message.stepProcess.done.read': 'Ler arquivos', + 'message.stepProcess.done.readImage': 'Ler imagens', + 'message.stepProcess.done.write': 'Escreveu arquivos', + 'message.stepProcess.done.search': 'Código pesquisado', + 'message.stepProcess.done.edit': 'Arquivos editados', + 'message.stepProcess.done.commands': 'Comandos executados', + 'message.stepProcess.done.code': 'Código executado', + 'message.stepProcess.done.webSearch': 'Pesquisei na web', + 'message.stepProcess.done.webFetch': 'Páginas da web visitadas', + 'message.stepProcess.done.subagents': 'Subagentes coordenados', + 'message.stepProcess.done.plan': 'Atualizado o plano', + 'message.stepProcess.done.questions': 'Perguntas feitas', + 'message.stepProcess.done.tools': 'Ferramentas chamadas', + 'message.stepProcess.joinTwo': '{first} e {second}', + 'message.stepProcess.comma': ', ', + 'message.stepProcess.sharedPrefix': '', + 'message.stepProcess.more': '{title}, etc.', + 'message.trigger.request': 'Execução solicitada', + 'message.trigger.goal': 'Objetivo contínuo', + 'message.trigger.agent': 'Mensagem de tarefa recebida', + 'message.trigger.team': 'Mensagem da equipe recebida', + 'message.trigger.subagent': 'Status da subtarefa atualizado', + 'message.trigger.github': 'GitHub evento recebido', + 'message.trigger.webhook': 'Evento externo recebido', + 'message.trigger.schedule': 'Tarefa de automação', + 'message.trigger.job': 'Tarefa em segundo plano atualizada', + 'message.trigger.plugin': 'Status do plug-in atualizado', + 'message.trigger.explanation': 'Esta notificação desencadeou esta resposta.', + 'message.turnProcess.worked': 'Concluído', + 'message.turnProcess.took': 'Concluído em ', + 'message.turnProcess.failed': 'Falha', + 'view.chat': 'Chat', + 'image.open': 'Ver imagem completa', + 'image.loading': 'Carregando imagem…', + 'image.failed': 'Visualização da imagem indisponível', + 'image.dialog': 'Visualização da imagem', + 'image.close': 'Fechar visualização da imagem', + 'number.groupSeparator': ',', + 'duration.compactSeconds': '{seconds}s', + 'duration.compactMinutes': '{minutes}m{seconds}s', + 'duration.milliseconds': '{milliseconds}ms', + 'stats.counts': '{turns} turnos, {steps} etapas', + 'stats.cacheHit': 'Acertos no cache: {percent}%', + 'stats.dialog.title': 'Estatísticas da sessão', + 'stats.dialog.usageTitle': 'Uso de token', + 'stats.dialog.llmTime': 'Horário do LLM', + 'stats.dialog.toolTime': 'Tempo da ferramenta', + 'stats.dialog.ttft': 'Tempo médio para o primeiro token (TTFT)', + 'stats.dialog.speed': 'Tokens por segundo (TPS)', + 'chat.loadingHistory': 'Carregando histórico…', + 'chat.loadError': 'Falha ao carregar o histórico: {message} ({code})', + 'chat.loadOlder': 'Carregar mais cedo', + 'chat.toBottom': 'De volta ao fundo', + 'chat.deepDiving': 'Raciocínio profundo', + 'chat.deepDivingFor': 'Raciocínio profundo há {duration} ···', + 'chat.turnNavigation.label': 'Navegação entre turnos', + 'chat.turnNavigation.jump': 'Ir para o turno {turn}', + 'chat.turnNavigation.jumpLoad': 'Carregar e ir para o turno {turn}', + 'chat.turnNavigation.turn': 'Turno {turn}', + 'settings.performance.title': 'Desempenho e uso', + 'settings.performance.description': 'Escolha quantas informações de desempenho e uso serão exibidas', + 'settings.performance.compact': 'Compacto', + 'settings.performance.detailed': 'Detalhado', + 'settings.links.title': 'Abra links de bate-papo em', + 'settings.links.description': 'Escolha onde abrir links da web', + 'settings.links.sidebar': 'Barra lateral no aplicativo', + 'settings.links.newTab': 'Navegador padrão', + 'settings.transcript.title': 'Detalhes do trabalho', + 'settings.transcript.description': 'Escolha quantos detalhes serão exibidos nas chamadas de ferramenta', + 'settings.collapse.title': 'Quando recolher detalhes do trabalho', + 'settings.collapse.description': 'Escolha quando recolher automaticamente os detalhes do trabalho', + 'settings.collapse.completion': 'Na conclusão', + 'settings.collapse.nextInput': 'Na próxima mensagem', + 'settings.foldedReplies.title': 'Respostas após o recolhimento', + 'settings.foldedReplies.description': 'Escolha quais respostas do assistente permanecerão visíveis quando os detalhes do trabalho forem recolhidos', + 'settings.foldedReplies.all': 'Todas as respostas', + 'settings.foldedReplies.final': 'Apenas resposta final', + 'settings.transcript.compact': 'Compacto', + 'settings.transcript.standard': 'Padrão', + 'settings.transcript.detailed': 'Detalhado', + 'settings.transcript.verbose': 'Verboso', + 'fileOpen.title': 'Não foi possível abrir o arquivo', + 'fileOpen.unknown': 'Não foi possível abrir este arquivo', + 'message.extraBlock': 'Bloco de conteúdo extra', + 'message.systemPrompt': 'Alerta do sistema', + 'message.systemPromptUpdate': 'Atualização imediata do sistema', + 'message.toolAdded': 'Ferramenta adicionada: {name}', + 'message.toolRemoved': 'Ferramenta removida: {name}', + 'message.toolsAdded': 'Adicionado: {names}', + 'message.toolsAddedCount': '{count} adicionado', + 'message.toolsChanged': '{added} adicionado, {removed} removido', + 'message.toolsRemoved': 'Removido: {names}', + 'message.toolsRemovedCount': '{count} removido', + 'message.toolsUpdated': 'Ferramentas atualizadas', + 'message.contextInjection': 'Injeção de contexto', + 'message.contextRecall': 'Recuperação de sessão', + 'message.referenceSummary': 'Sessão referenciada · {labels}', + 'message.referenceSeparator': ', ', + 'message.context.instructions.loaded': 'carregado', + 'message.context.instructions.added': 'adicionado', + 'message.context.instructions.updated': 'atualizado', + 'message.context.instructions.removed': 'removido', + 'message.context.catalog.replaced': 'Catálogo de substituição', + 'message.context.catalog.more': '… {count} mais', + 'message.context.snapshot.supersedes': 'Substitui instantâneos anteriores', + 'message.context.relay.from': 'Da sessão {session}', + 'message.context.recall.counts': '{retained} mantido · {omitted} omitido', + 'message.context.recall.truncated': 'truncado', + 'message.compaction': 'Contexto compactado', + 'message.compaction.running': 'Contexto compactado…', + 'message.compaction.completed': '{items} itens de histórico compactados (~{tokens} tokens)', + 'message.compaction.expand': 'Ver resumo de compactação', + 'message.compaction.unavailable': 'Resumo de compactação indisponível', + 'message.compaction.commandTitle': 'compacto', + 'message.think': 'Pense', + 'message.unknownSurface': 'Evento de superfície desconhecido: {type}', + 'message.unknownBlock': 'Bloco de conteúdo desconhecido', + 'message.turnProcess.toolCalls.one': '{count} chamada de ferramenta', + 'message.turnProcess.toolCalls.other': '{count} chamadas de ferramenta', + 'message.turnProcess.messages.one': '{count} mensagem', + 'message.turnProcess.messages.other': '{count} mensagens', + 'message.turnProcess.subagents.one': '{count} subagente', + 'message.turnProcess.subagents.other': '{count} subagentes', + 'message.turnProcess.thoughtForAWhile': 'Pensou por um tempo', + 'message.turnProcess.separator': ' · ', + 'message.stopped': 'Parado', + 'message.branch': 'Ramifique para uma nova conversa', + 'message.branchUnavailable': 'Disponível apenas na última mensagem de um turno concluído', + 'message.retry.active': 'Tentando novamente a solicitação de modelo', + 'message.retry.cancelled': 'Nova tentativa de solicitação de modelo cancelada', + 'message.retry.started': 'Solicitação de modelo repetida', + 'message.retry.scheduled': 'Aguardando para tentar novamente a solicitação de modelo', + 'message.retry.status': '{label} ({retry}/{maximum}) · {seconds}s', + 'message.retry.delay': 'Atraso na nova tentativa: ', + 'message.retry.failure': 'Motivo da falha: ', + 'message.failure.auth': 'A chave API é inválida', + 'message.taskStopped': 'Tarefa interrompida', + 'message.failure.accountSignedOut': 'Interrompido porque você saiu de DeepSeek.', + 'message.failure.interrupted': 'O processo DSH foi encerrado antes do término deste turno (por exemplo, ele foi eliminado, travou ou expirou durante o desligamento), portanto, o turno foi encerrado quando DSH foi reiniciado. Chamadas de ferramentas inacabadas têm resultados desconhecidos.', + 'message.failure.accountSignInRequired': 'Faça login em DeepSeek e verifique se o destino da solicitação suporta autenticação de conta.', + 'message.failure.quota': 'Cota de solicitação esgotada.', + 'message.turnError': 'Este turno falhou', + 'message.maxTokens': 'Limite de token de saída atingido', + 'message.maxTokens.hint': 'A resposta foi cortada; a saída anterior é preservada na conversa. Envie "continuar" para permitir que o modelo seja retomado.', + 'message.tokensPerSecond': '{tps} tok/s', + 'message.turnUsage.title': 'Uso do turno', + 'message.turnUsage.consumed': 'Uso {total}', + 'message.turnUsage.model': 'Provedor/modelo', + 'message.turnUsage.cacheHit': 'Acertos no cache', + 'message.turnUsage.input': 'Entrada sem cache', + 'message.turnUsage.cacheRead': 'Entrada em cache', + 'message.turnUsage.cacheWrite': 'Gravação em cache', + 'message.turnUsage.output': 'Saída', + 'message.turnUsage.reasoning': ' ({tokens} raciocínio)', + 'message.turnUsage.count': '{count} tok', + 'duration.secondUnit': 's', + 'duration.minuteUnit': 'm ', + 'duration.hourUnit': 'h ', + 'command.running': 'Correndo…', + 'command.failed': 'Falha no comando', + 'command.done': 'Concluído', + 'command.title': 'Comando', + 'row.running': 'Correndo', + 'row.failed': 'Falha', + 'json.truncated': '… truncado, {total} caracteres no total', + 'clock.md': '{m}/{d}', + 'clock.ymd': '{y}-{m}-{d}', +} satisfies Record diff --git a/packages/client/ui-chat/tests/chat-stats.client.spec.tsx b/packages/client/ui-chat/tests/chat-stats.client.spec.tsx index 79281d39d1..8d63088052 100644 --- a/packages/client/ui-chat/tests/chat-stats.client.spec.tsx +++ b/packages/client/ui-chat/tests/chat-stats.client.spec.tsx @@ -268,7 +268,7 @@ describe('composer stats pills', () => { expect(dialog.getAttribute('aria-label')).toBe('Session statistics') // Portaled out of the composer dock. expect(dialog.parentElement).toBe(document.body) - expect(dialog.firstChild?.textContent).toBe('Session statistics') + expect(dialog.firstChild?.textContent).toBe('Session statistics1 turns 1 steps') const details = dialog.querySelector('[data-session-stats-details]') as HTMLElement expect(details).toBeTruthy() expect(details.textContent).toContain('LLM time3.8s') @@ -375,6 +375,53 @@ describe('composer stats pills', () => { expect(usageDialog.textContent).toContain('未缓存输入5 tok') }) + describe('full reading of a cut-off pill', () => { + const FULL = '105 tok · Cache hit 90%' + + function renderCut(overflow: boolean, mode: 'detailed' | 'compact' = 'detailed') { + vi.spyOn(Element.prototype, 'scrollWidth', 'get').mockReturnValue(overflow ? 200 : 80) + vi.spyOn(Element.prototype, 'clientWidth', 'get').mockReturnValue(80) + vi.useFakeTimers() + const { source } = makeSource({ nodes: [timedStep()] }) + return render( selector(mode)} />) + } + const tooltipText = (view: ReturnType) => view.queryByRole('tooltip')?.textContent ?? null + afterEach(() => { vi.useRealTimers() }) + + it.each([false, true])('shows on hover after the dock delay only while the label is cut off: %s', (overflow) => { + const view = renderCut(overflow) + fireEvent.pointerEnter(view.getAllByRole('button')[1]!) + expect(tooltipText(view)).toBeNull() + act(() => { vi.advanceTimersByTime(200) }) + expect(tooltipText(view)).toBe(overflow ? FULL : null) + }) + + it('shows on keyboard focus of a dialog pill', () => { + const view = renderCut(true) + fireEvent.keyDown(document.body, { key: 'Tab' }) + fireEvent.focus(view.getAllByRole('button')[1]!) + expect(tooltipText(view)).toBe(FULL) + }) + + it('yields to the open dialog, whose head carries the counts', () => { + const view = renderCut(true) + const [timePill] = view.getAllByRole('button') as [HTMLElement] + fireEvent.click(timePill) + fireEvent.pointerEnter(timePill) + act(() => { vi.advanceTimersByTime(200) }) + expect(tooltipText(view)).toBeNull() + expect(view.getByRole('dialog').firstChild?.textContent).toBe('Session statistics1 turns 1 steps') + }) + + it('pins a cut-off static reading on tap', () => { + const view = renderCut(true, 'compact') + const plainPill = view.getByText('Cache hit 90%').parentElement! + fireEvent.pointerDown(plainPill, { pointerType: 'touch' }) + fireEvent.click(plainPill) + expect(tooltipText(view)).toBe('Cache hit 90%') + }) + }) + it('keeps the durable usage pill after the visible step window is empty', () => { const { source } = makeSource() const view = render( { }) const fileDrag = { prepare: vi.fn(async () => ({ start: () => true, release: () => {} })), reportFailure: vi.fn() } + +it('formats Portuguese elapsed minutes with numeric units', () => { + expect(formatRunDuration(90_000, key => pt[key])).toBe('1m 30s') +}) diff --git a/packages/client/ui-commands/locale/de.json b/packages/client/ui-commands/locale/de.json new file mode 100644 index 0000000000..8fdc53edce --- /dev/null +++ b/packages/client/ui-commands/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Befehlsauswahlschnittstelle", + "description": "Bietet ein Slash-Befehlsmenü und eine zugehörige Bedienoberfläche im Eingabefeld." + } +} diff --git a/packages/client/ui-commands/locale/es.json b/packages/client/ui-commands/locale/es.json new file mode 100644 index 0000000000..9fb9b09c55 --- /dev/null +++ b/packages/client/ui-commands/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de selección de comandos", + "description": "Proporciona un menú de comando de barra diagonal y una interfaz de operación relacionada en el cuadro de entrada." + } +} diff --git a/packages/client/ui-commands/locale/fr.json b/packages/client/ui-commands/locale/fr.json new file mode 100644 index 0000000000..e0e61417a6 --- /dev/null +++ b/packages/client/ui-commands/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de sélection de commandes", + "description": "Fournit un menu de commande slash et une interface d’opération associée dans la zone de saisie." + } +} diff --git a/packages/client/ui-commands/locale/pt.json b/packages/client/ui-commands/locale/pt.json new file mode 100644 index 0000000000..01fc89d3a8 --- /dev/null +++ b/packages/client/ui-commands/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de seleção de comandos", + "description": "Fornece um menu de comando de barra e interface de operação relacionada na caixa de entrada." + } +} diff --git a/packages/client/ui-commands/locale/ru.json b/packages/client/ui-commands/locale/ru.json new file mode 100644 index 0000000000..529ec9773b --- /dev/null +++ b/packages/client/ui-commands/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс выбора команд", + "description": "Предоставляет меню команд с косой чертой и соответствующий рабочий интерфейс в поле ввода." + } +} diff --git a/packages/client/ui-commands/src/client/index.ts b/packages/client/ui-commands/src/client/index.ts index fc098764a4..f1e8d62bf2 100644 --- a/packages/client/ui-commands/src/client/index.ts +++ b/packages/client/ui-commands/src/client/index.ts @@ -18,7 +18,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-session/client' import { CommandUiRuntime } from './service.ts' import type { PopupSelectInjected } from './PopupSelectView.tsx' import { PopupSelectView } from './PopupSelectView.tsx' -import { en, ja, zh, type CommandKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type CommandKey } from './locales.ts' export { CommandUiRuntime } from './service.ts' export { CommandDirectory } from './directory.ts' @@ -56,7 +56,7 @@ export const inject = ['inputTriggers', 'sessions', 'remote', 'remote.commands', * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-commands: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-commands: dictionaries') ctx.plugin(CommandUiRuntime) ctx.inject(['slots', 'commandUi', 'sessions'], (scope: ClientContext) => { const command = scope.commandUi diff --git a/packages/client/ui-commands/src/client/locales.ts b/packages/client/ui-commands/src/client/locales.ts index de34e25cb7..ae0517b5f4 100644 --- a/packages/client/ui-commands/src/client/locales.ts +++ b/packages/client/ui-commands/src/client/locales.ts @@ -103,3 +103,163 @@ export const ja = { 'listbox.aria': '/{command} の一致項目', 'notice.attachmentsUnsupported': '/{command} は添付ファイルを受け付けません。先に削除してください', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'section.add': 'Añadir', + 'section.commands': 'Comandos', + 'label.goal': 'Objetivo', + 'label.plan': 'Planificar', + 'label.feedback': 'Comentarios', + 'label.compact': 'Compacto', + 'label.permission': 'Permiso', + 'label.export': 'Exportar', + 'description.goal': 'Establecer o ver el objetivo de una tarea de larga duración', + 'description.plan': 'Entrar o salir del modo plan', + 'description.feedback': 'Registrar comentarios sobre esta sesión', + 'description.compact': 'Historial de conversaciones antiguo compacto', + 'description.permission': 'Cambiar el ajuste preestablecido de permisos (modo sandbox + política de aprobación)', + 'description.export': 'Descargue este registro de sesión como un archivo ZIP', + 'token.goal': 'objetivo', + 'token.plan': 'planear', + 'token.feedback': 'retroalimentación', + 'token.compact': 'compacto', + 'token.permission': 'permiso', + 'token.export': 'exportar', + 'search.placeholder': 'Buscar…', + 'search.aria': 'Opciones de filtro', + 'status.loading': 'Cargando opciones…', + 'status.applying': 'Aplicando…', + 'status.empty': 'Sin opciones', + 'overlay.aria': 'Opciones de /{command}', + 'listbox.aria': '/{command} coincidencias', + 'notice.attachmentsUnsupported': '/{command} no acepta archivos adjuntos; eliminarlos primero', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'section.add': 'Ajouter', + 'section.commands': 'Commandes', + 'label.goal': 'Objectif', + 'label.plan': 'Planifier', + 'label.feedback': 'Commentaires', + 'label.compact': 'Compacte', + 'label.permission': 'Autorisation', + 'label.export': 'Exporter', + 'description.goal': "Définir ou afficher l'objectif d'une tâche de longue durée", + 'description.plan': 'Entrer ou quitter le mode Plan', + 'description.feedback': 'Enregistrer les commentaires sur cette session', + 'description.compact': 'Compacter l’historique des conversations anciennes', + 'description.permission': "Changer le préréglage d'autorisation (mode sandbox + politique d'approbation)", + 'description.export': "Téléchargez ce journal de session sous forme d'archive ZIP", + 'token.goal': 'objectif', + 'token.plan': 'planifier', + 'token.feedback': 'commentaires', + 'token.compact': 'compacte', + 'token.permission': 'autorisation', + 'token.export': 'exporter', + 'search.placeholder': 'Rechercher…', + 'search.aria': 'Options de filtrage', + 'status.loading': 'Options de chargement…', + 'status.applying': 'Application…', + 'status.empty': 'Aucune option', + 'overlay.aria': 'Options /{command}', + 'listbox.aria': '/{command} correspondances', + 'notice.attachmentsUnsupported': "/{command} n'accepte pas les pièces jointes ; retirez-les d'abord", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'section.add': 'Hinzufügen', + 'section.commands': 'Befehle', + 'label.goal': 'Ziel', + 'label.plan': 'Planen', + 'label.feedback': 'Rückmeldung', + 'label.compact': 'Kompakt', + 'label.permission': 'Erlaubnis', + 'label.export': 'Exportieren', + 'description.goal': 'Legen Sie das Ziel für eine Aufgabe mit langer Laufzeit fest oder zeigen Sie es an', + 'description.plan': 'Aktivieren oder verlassen Sie den Planmodus', + 'description.feedback': 'Zeichnen Sie Feedback zu dieser Sitzung auf', + 'description.compact': 'Kompakter älterer Gesprächsverlauf', + 'description.permission': 'Ändern Sie die Berechtigungsvoreinstellung (Sandbox-Modus + Genehmigungsrichtlinie).', + 'description.export': 'Laden Sie dieses Sitzungsprotokoll als ZIP-Archiv herunter', + 'token.goal': 'Ziel', + 'token.plan': 'planen', + 'token.feedback': 'Rückmeldung', + 'token.compact': 'kompakt', + 'token.permission': 'Erlaubnis', + 'token.export': 'exportieren', + 'search.placeholder': 'Suchen…', + 'search.aria': 'Filteroptionen', + 'status.loading': 'Ladeoptionen…', + 'status.applying': 'Wird angewendet…', + 'status.empty': 'Keine Optionen', + 'overlay.aria': '/{command}-Optionen', + 'listbox.aria': '/{command} stimmt überein', + 'notice.attachmentsUnsupported': '/{command} akzeptiert keine Anhänge; Entfernen Sie sie zuerst', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'section.add': 'Добавить', + 'section.commands': 'Команды', + 'label.goal': 'Цель', + 'label.plan': 'План', + 'label.feedback': 'Обратная связь', + 'label.compact': 'Компактный', + 'label.permission': 'Разрешение', + 'label.export': 'Экспорт', + 'description.goal': 'Установите или просмотрите цель для долгосрочной задачи', + 'description.plan': 'Войти в режим плана или выйти из него', + 'description.feedback': 'Запишите отзыв об этом сеансе', + 'description.compact': 'Сжать старую историю разговоров', + 'description.permission': 'Переключение настроек разрешений (режим песочницы + политика утверждения)', + 'description.export': 'Загрузите этот журнал сеанса в виде ZIP-архива.', + 'token.goal': 'цель', + 'token.plan': 'план', + 'token.feedback': 'обратная связь', + 'token.compact': 'компактный', + 'token.permission': 'разрешение', + 'token.export': 'экспорт', + 'search.placeholder': 'Искать…', + 'search.aria': 'Параметры фильтра', + 'status.loading': 'Загрузка параметров…', + 'status.applying': 'Применение…', + 'status.empty': 'Нет вариантов', + 'overlay.aria': 'Опции /{command}', + 'listbox.aria': '/{command} совпадений', + 'notice.attachmentsUnsupported': '/{command} не принимает вложения; сначала удали их', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'section.add': 'Adicionar', + 'section.commands': 'Comandos', + 'label.goal': 'Objetivo', + 'label.plan': 'Plano', + 'label.feedback': 'Comentários', + 'label.compact': 'Compacto', + 'label.permission': 'Permissão', + 'label.export': 'Exportar', + 'description.goal': 'Defina ou visualize a meta para uma tarefa de longa duração', + 'description.plan': 'Entrar ou sair do modo de plano', + 'description.feedback': 'Registre feedback sobre esta sessão', + 'description.compact': 'Histórico compacto de conversas antigas', + 'description.permission': 'Alterar a predefinição de permissão (modo sandbox + política de aprovação)', + 'description.export': 'Baixe este log de sessão como um arquivo ZIP', + 'token.goal': 'objetivo', + 'token.plan': 'planejar', + 'token.feedback': 'comentários', + 'token.compact': 'compacto', + 'token.permission': 'permissão', + 'token.export': 'exportar', + 'search.placeholder': 'Pesquisar…', + 'search.aria': 'Opções de filtro', + 'status.loading': 'Carregando opções…', + 'status.applying': 'Aplicando…', + 'status.empty': 'Sem opções', + 'overlay.aria': '/{command} opções', + 'listbox.aria': '/{command} correspondências', + 'notice.attachmentsUnsupported': '/{command} não aceita anexos; remova-os primeiro', +} satisfies Record diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 1ba26214ba..7d9bf9f3c1 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -21,8 +21,8 @@ en: 7871f9fe6bcc4459 zh: 0d51565c600b3958 /deepseek-ai-dsh-client-ui-conversation/shell-and-standard-props: - en: 6ceacacf8770eec9 - zh: 4864c23171b99a82 + en: e910ac7fec4fd72d + zh: 741a399436dc98d9 /deepseek-ai-dsh-client-ui-conversation/temporary-composer-entries: en: bb1244310f5c0382 zh: 824b84077432b559 diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 4bcc57e1cd..86a1ffd236 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -82,10 +82,12 @@ The context-occupancy button shows a ring and percentage below the input card, a With Coding Tools off, the shell hides only Trajectory; other plugin-contributed Views remain available. The View tab bar is hidden when fewer than two Views are available. Turning Coding Tools off while Trajectory is active returns to Chat; stored View preferences and recorded Session data remain intact. Turning it on makes Trajectory available again. View owners receive the available roster so their navigation actions can follow the same visibility. -While the root-scoped `conversation.session.header.views.actions` list has registrations, a **More conversation views** button with a tooltip follows the View tabs in the tab row. It opens a menu whose rows are that list's registrations. Each receives `entries` (available Views in registration order with `id`, `label`, and `pinned`; Chat is never listed), `onOpen(id)`, which selects the View in this Session, `onPinnedChange(id, pinned)`, and `onDismiss()`, which closes the menu. The header keeps one root list of unpinned View ids in browser `localStorage` under `dsh.slot.manual-order.conversation.view.v1`, stored as `{ "unpinned": string[] }` and shared by every Session; the tab bar omits unpinned Views, and Chat always shows. The tab bar's visibility counts available Views, not pinned ones, so the bar and More stay while every other View is unpinned. Unpinning the active View leaves its content selected and keeps its tab, with `aria-selected`, until another View is selected. Ids of Views that are not registered stay stored; stored content other than a string array shows every tab. +While the root-scoped `conversation.session.header.views.actions` list has registrations, a **More conversation views** button with a tooltip follows the View tabs in the tab row. It opens a menu whose rows are that list's registrations. Each receives `entries` (Views outside the header in registration order with `id`, `label`, `pinned`, and `active`; Chat is never listed), `onOpen(id)`, which selects the View in this Session, `onPinnedChange(id, pinned)`, and `onDismiss()`, which closes the menu. The header keeps an in-memory list of unpinned View ids shared by every Session; the tab bar omits unpinned Views and places registered Chat first so it always remains outside More. Plugin reload resets pin choices, and legacy localStorage values are ignored. The tab bar's visibility counts available Views, not pinned ones, so the bar and More stay while every other View is unpinned. Unpinning the active View hides its tab immediately without changing the selected content. More lists only Views outside the header and identifies the current View through `active`. The header reads its available row width, reserves 48px for More and padding, and estimates 120px per tab. Remaining Views keep registration order in More. A single ResizeObserver watches the tab row so window resizing and sidebar expansion update the split; active selection does not change the order. The observer disconnects when the row is removed. If ResizeObserver is unavailable, the initial width calculation still applies without subsequent observation. The More button highlights and names the active View in its tooltip when that View is folded. Individual tabs are not measured. At least one preferred tab remains outside, and long labels truncate with a full-name tooltip. Unregistered View ids remain in the in-memory pin list until plugin reload. The composer registers the File command action and owns its label, availability, and native file-dialog callback. Menu availability and invocation both consult the mounted composer's current attachment-intake policy. Unmounting or locking the composer disables that action; disposing the plugin removes its registration. The callback binding stays inside the input module. +The composer reads its Session's access answers through the Session-scope `useAccess` Hook: Send disappears while the access snapshot excludes `session/prompt`, Stop while it excludes `session/cancel`, and file intake follows `fileUploads/upload`. A composer without a Session has no answer and keeps its ordinary controls. Send and Stop are withheld from their buttons alone: a keyboard submission or the Esc-Esc sequence still reaches the Host, which refuses it and reports the failure. These UI answers never replace Host authorization. + `SessionInputShell` owns one Lexical editor per Session through its private [DraftEditorRuntime](src/client/input/editor/runtime.ts), while retaining submission, attachment selection, and recovery decisions. [DraftEditor](src/client/input/editor/DraftEditor.tsx) renders the borrowed editor; InputBar retains its Hooks and refs and installs DOM behavior through [view-binding](src/client/input/editor/view-binding.ts). Editor-facing types live in [draft-editor.ts](src/client/contract/draft-editor.ts), with shared input and submission types in [input.ts](src/client/contract/input.ts). Background editor updates retain the draft selection without changing the document selection or reclaiming focus from another control; explicit Composer focus restores its selection. This separation does not support simultaneous editable roots for one Session; [the two-stage isolation proposal](../../../.agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.md) defines the remaining work. Drafts retain plain text and file, folder, and Session reference chips across Session switching and reload. Public input methods accept text; internal restoration and Workspace transfer preserve structured references. The input model imports saved content before its first published read; React only mounts that editor and binds the existing Conversation store writer. The original storage key accepts legacy strings and writes semantic snapshots without runtime node IDs. A pending skill catalog does not block editing: slash names remain ordinary text until a matching catalog entry arrives. Arrival rematches the current text without changing chips, selection, or Undo history. Browser-owned attachment File objects are not persisted by this mechanism. The [draft initialization decision](../../../.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.md) defines ownership and restoration. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index 57c499aaf8..ebfbaff130 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -82,10 +82,12 @@ composer 在挂载、切换 Session 和解除锁定时使用共享的[文字输 关闭代码工作工具时,外壳仅隐藏 Trajectory;其他插件贡献的 View 仍然可用。可用 View 少于两个时隐藏 View 标签栏。在 Trajectory 激活时关闭代码工作工具会返回 Chat;已保存的 View 偏好和 Session 记录保持不变。开启后,Trajectory 恢复可用。View 所有者接收可用列表,使导航操作跟随相同的可见性。 -root 作用域的 `conversation.session.header.views.actions` list 有注册项时,标签行中的 View 标签之后显示带提示框的**更多会话视图**按钮。它打开的菜单以该 list 的注册项为菜单行。每个注册项接收 `entries`(按注册顺序的可用 View,含 `id`、`label` 与 `pinned`;不含 Chat)、在本 Session 选择 View 的 `onOpen(id)`、`onPinnedChange(id, pinned)`,以及关闭菜单的 `onDismiss()`。头部在浏览器 `localStorage` 的 `dsh.slot.manual-order.conversation.view.v1` 下保存一份 root 级的已收起 View id 列表,格式为 `{ "unpinned": string[] }`,所有 Session 共用;标签栏不显示已收起的 View,Chat 始终显示。标签栏是否显示按可用 View 计算,不按已固定的 View 计算,因此其他 View 全部收起时,标签栏和“更多”仍然显示。收起当前激活的 View 不会切走其内容,其标签及 `aria-selected` 保留到选中其他 View 为止。未注册 View 的 id 保留在列表中;保存内容不是字符串数组时,所有标签都显示。 +root 作用域的 `conversation.session.header.views.actions` list 有注册项时,标签行中的 View 标签之后显示带提示框的**更多会话视图**按钮。它打开的菜单以该 list 的注册项为菜单行。每个注册项接收 `entries`(按注册顺序的头部之外的 View,含 `id`、`label`、`pinned` 与 `active`;不含 Chat)、在本 Session 选择 View 的 `onOpen(id)`、`onPinnedChange(id, pinned)`,以及关闭菜单的 `onDismiss()`。头部在内存中保存所有 Session 共用的已收起 View id 列表;标签栏不显示已收起的 View,并将已注册的 Chat 放在首位,确保它始终留在更多菜单外。插件重新加载时 Pin 选择重置,不读取旧 localStorage 值。标签栏是否显示按可用 View 计算,不按已固定的 View 计算,因此其他 View 全部收起时,标签栏和“更多”仍然显示。收起当前激活的 View 会立即隐藏其标签,但不会切走其内容。更多菜单仅包含头部之外的 View,通过 `active` 标识当前 View。头部读取当前行可用宽度,为更多按钮与内边距预留 48px,每个标签估算 120px,其余 View 按注册顺序进入更多。单个 ResizeObserver 观察标签行,使窗口缩放和侧栏展开能够更新分组;当前选中项变化不改变顺序。标签行移除时断开观察。如果环境没有 ResizeObserver,仍按初始宽度计算,但不继续观察。当前 View 收起时,更多按钮高亮并在提示框中显示其名称。不逐项测量标签。至少保留一个优先展示的外部标签,长标题截断并通过提示框提供全名。未注册 View 的 id 保留在内存 Pin 列表中,直到插件重新加载。 输入框注册「文件」命令动作,负责其标题、可用性和原生文件选择器回调。菜单可用性与实际调用都读取已挂载输入框当前的附件接收策略。输入框卸载或锁定后该动作不可用,插件 dispose(资源释放)时移除注册。回调绑定留在输入模块内部。 +输入框通过会话作用域的 `useAccess` 钩子读取所在 Session 的访问答案:访问快照不含 `session/prompt` 时隐藏发送,不含 `session/cancel` 时隐藏停止,附件接收跟随 `fileUploads/upload`。没有 Session 的输入框尚无答案,保留普通控件。发送与停止都只对按钮生效——键盘提交或 Esc-Esc 序列仍会到达 Host,由 Host 拒绝并上报失败。这些界面判断不替代 Host 授权。 + `SessionInputShell` 通过私有 [DraftEditorRuntime](src/client/input/editor/runtime.ts) 为每个 Session 持有一个 Lexical editor,同时保留提交、附件选择和恢复决策。[DraftEditor](src/client/input/editor/DraftEditor.tsx) 呈现借用的 editor;InputBar 保留钩子与 refs,并通过 [view-binding](src/client/input/editor/view-binding.ts) 安装 DOM 行为。编辑器类型位于 [draft-editor.ts](src/client/contract/draft-editor.ts),共享输入和提交类型位于 [input.ts](src/client/contract/input.ts)。编辑器后台更新会保留草稿选区,不修改文档选区或从其他控件夺回焦点;显式聚焦 Composer 时恢复其选区。这一拆分不支持同一 Session 同时挂载多个可编辑 root;[两阶段隔离提案](../../../.agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.zh.md) 定义剩余工作。 草稿中的普通文字及文件、目录、Session 引用胶囊在切换 Session 和刷新后保留。公开输入方法接受文本;内部恢复和工作区携带保留结构化引用。输入模型在首次对外读取前导入保存内容;React 只挂载同一编辑器并绑定现有 Conversation store 写入回调。原存储 key 兼容旧字符串,新写入保存不含运行时节点 ID 的语义快照。等待 skill 目录不会阻塞编辑:斜杠名称保持普通文字,直到匹配的目录项到达。目录到达只重匹配当前文字,不改变胶囊、选区或 Undo 历史。浏览器持有的附件 File 对象不由这一机制持久化。[草稿初始化决策](../../../.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.zh.md)规定所有权与恢复流程。 diff --git a/packages/client/ui-conversation/locale/de.json b/packages/client/ui-conversation/locale/de.json new file mode 100644 index 0000000000..def54f49a5 --- /dev/null +++ b/packages/client/ui-conversation/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Konversationsschnittstellen-Framework", + "description": "Organisieren Sie Dialoginhalte, Eingabefelder, Nachrichtenwarteschlangen und zeigen Sie die Navigation an." + } +} diff --git a/packages/client/ui-conversation/locale/es.json b/packages/client/ui-conversation/locale/es.json new file mode 100644 index 0000000000..89664f8f6a --- /dev/null +++ b/packages/client/ui-conversation/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "marco de interfaz conversacional", + "description": "Organice el contenido del diálogo, los cuadros de entrada, las colas de mensajes y vea la navegación." + } +} diff --git a/packages/client/ui-conversation/locale/fr.json b/packages/client/ui-conversation/locale/fr.json new file mode 100644 index 0000000000..a24689adf7 --- /dev/null +++ b/packages/client/ui-conversation/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "cadre d'interface conversationnelle", + "description": "Organisez le contenu des boîtes de dialogue, les zones de saisie, les files d'attente de messages et la navigation dans les vues." + } +} diff --git a/packages/client/ui-conversation/locale/pt.json b/packages/client/ui-conversation/locale/pt.json new file mode 100644 index 0000000000..25642a6dc7 --- /dev/null +++ b/packages/client/ui-conversation/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "estrutura de interface conversacional", + "description": "Organize o conteúdo da caixa de diálogo, caixas de entrada, filas de mensagens e visualize a navegação." + } +} diff --git a/packages/client/ui-conversation/locale/ru.json b/packages/client/ui-conversation/locale/ru.json new file mode 100644 index 0000000000..5e527b7f33 --- /dev/null +++ b/packages/client/ui-conversation/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "структура диалогового интерфейса", + "description": "Упорядочивайте содержимое диалогов, поля ввода, очереди сообщений и навигацию по просмотрам." + } +} diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts index 6a004c8740..897347729f 100644 --- a/packages/client/ui-conversation/src/client/apply.ts +++ b/packages/client/ui-conversation/src/client/apply.ts @@ -46,7 +46,7 @@ import { todoDockEntry } from './skeleton/TodoPanel.tsx' import { installStopShortcut } from './stop-shortcut.ts' import { TRAJECTORY_VIEW_ID, resolveActiveView } from './view-selection.ts' import { createViewPins } from './view-pins.ts' -import { en, ja, NS, zh, type ConversationKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type ConversationKey } from './locales.ts' import { CONVERSATION_SETTINGS_NAMESPACE, type ConversationSettings } from '../submission-settings.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { @@ -158,7 +158,7 @@ export function apply(ctx: Context, config: Config = Config({})): void { const slots = ctx.slots // Schemastery's field default is materialized before Cordis calls apply. const maxConcurrentFileUploads = config.maxConcurrentFileUploads as number - const workspaceNavigation = ctx.get('uiWorkspace') as unknown as WorkspaceNavigation + const workspaceNavigation = ctx.get('uiWorkspace') as WorkspaceNavigation const uiConversation = new UiConversation(ctx, sessions) uiConversation.userText.register(sessionReferenceRule) uiConversation.userText.register(collapseWhitespaceRule) @@ -185,7 +185,7 @@ export function apply(ctx: Context, config: Config = Config({})): void { yield slots.register({ name: 'conversation.user.text.node', key: 'session', priority: 100 }, SessionReference) }) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-conversation: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-conversation: dictionaries') const t = ctx.locale.bind(NS) const conversationStore = createConversationStore() const submissionPolicy = new ComposerSubmissionPolicy( diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index f8d86187d3..3bf918336e 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -448,13 +448,15 @@ export interface ConversationViewActionEntry { readonly id: string /** Resolved View label. */ readonly label: string - /** False while the tab is unpinned from the header. */ + /** False while the View is manually unpinned. A pinned View still enters More when the header's width budget cannot hold it. */ readonly pinned: boolean + /** Whether this View is selected in this Session, independently of its pin state. */ + readonly active?: boolean } /** Owner share of the View tab row's More menu rows. */ export interface ConversationViewActionsOwnerProps { - /** Available Views in registration order; Chat is never listed. */ + /** Views outside the header in registration order. */ readonly entries: readonly ConversationViewActionEntry[] /** * Select a View in this Session without changing its pin state. @@ -462,9 +464,9 @@ export interface ConversationViewActionsOwnerProps { */ readonly onOpen: (id: string) => void /** - * Show (true) or unpin (false) a View tab in every Session. + * Pin or unpin a View in every Session. * @param id - Listed View id. - * @param pinned - Whether the tab shows in the header. + * @param pinned - Whether the View is manually pinned; the width budget can still fold it into More. */ readonly onPinnedChange: (id: string, pinned: boolean) => void /** Close the More menu. */ @@ -476,15 +478,15 @@ export interface ConversationSessionHeaderInjected { /** Package-owned View roster source bound only for the Conversation header. */ readonly hooks: { readonly conversationViews: ObservableSnapshot - /** Ids of unpinned View tabs, persisted across reloads and shared by every Session. */ + /** Ids of unpinned View tabs held in memory and shared by every Session until plugin reload. */ readonly entryPins: ObservableSnapshot /** Whether `conversation.session.header.views.actions` has registrations. */ readonly entryActions: ObservableSnapshot } /** - * Show or unpin one View tab in every Session; Chat is ignored. + * Pin or unpin one View tab in every Session; Chat is ignored. * @param id - View id. - * @param pinned - Whether the tab shows in the header. + * @param pinned - Whether the View is manually pinned; the width budget can still fold it into More. */ setPinned: (id: string, pinned: boolean) => void /** Select a Session through the Session Controller. */ diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index cde0c51e72..6fdbd406fd 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -80,6 +80,7 @@ export const zh = { 'hero.chooseWorkspace': '选择工作区', 'session.hierarchy': '会话层级', 'session.moreViews': '更多会话视图', + 'session.moreViewsCurrent': '更多会话视图 · {name}', 'todo.title': '任务', 'todo.progress.done': '{done} 已完成', 'todo.progress.active': '{active} 进行中', @@ -462,6 +463,7 @@ export const en = { 'hero.chooseWorkspace': 'Choose workspace', 'session.hierarchy': 'Session hierarchy', 'session.moreViews': 'More conversation views', + 'session.moreViewsCurrent': 'More conversation views · {name}', 'todo.title': 'To-dos', 'todo.progress.done': '{done} completed', 'todo.progress.active': '{active} in progress', @@ -843,6 +845,7 @@ export const ja = { 'hero.chooseWorkspace': 'ワークスペースを選択', 'session.hierarchy': 'セッション階層', 'session.moreViews': 'その他の会話ビュー', + 'session.moreViewsCurrent': 'その他の会話ビュー · {name}', 'todo.title': 'ToDo', 'todo.progress.done': '{done}件完了', 'todo.progress.active': '{active}件進行中', @@ -1148,3 +1151,1908 @@ export const ja = { 'terminal.sendInput': '(入力を送信)', 'terminal.session': 'ターミナル {sessionId}', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'quote.remove': 'Eliminar cotización', + 'shortcut.newline': 'Nueva linea', + 'shortcut.complementary': 'Usar la acción alternativa de poner en cola o redirigir', + 'shortcut.slash': 'Abrir menú de comandos', + 'shortcut.mention': 'Abrir menú de referencia', + 'hint.plan': PLAN_NEXT_ACTION_EN, + 'hint.goal': 'describir el objetivo de una tarea de larga duración', + 'hint.goal.active': 'Objetivo activo: editar/pausar/reanudar/borrar', + 'placeholder.plan': PLAN_NEXT_ACTION_EN, + 'placeholder.default': 'Envía mensajes o ejecuta una tarea, / comandos, @ archivos o sesiones', + 'placeholder.unavailable': 'Sesión no disponible', + 'placeholder.parentOffline': 'Sesión para padres sin conexión; El envío no está disponible pero aún puedes detener la ejecución.', + 'placeholder.hero': 'Describe lo que quieres construir, / comandos, @ archivos o sesiones', + 'placeholder.workspace': 'Elija un espacio de trabajo para comenzar', + 'placeholder.steerQueue': 'Cmd/Ctrl+Enter dirige todos los mensajes en cola', + 'input.commands': 'Agregar archivos o ejecutar comandos', + 'input.file': 'Archivo', + 'input.stop': 'Detener generación', + 'input.send': 'Enviar mensaje', + 'input.send.queue': 'mensaje de cola', + 'input.send.steer': 'Redirigir mensaje', + 'attachment.pending': 'Anexos pendientes', + 'attachment.scrollLeft': 'Desplazar archivos adjuntos hacia la izquierda', + 'attachment.scrollRight': 'Desplazar archivos adjuntos hacia la derecha', + 'attachment.dropTitle': 'Arrastre archivos o imágenes aquí para agregarlos', + 'attachment.dropDesc': 'Límite de imágenes: hasta imágenes {count}, {size} cada una', + 'attachment.dropBlocked': 'No se pueden agregar archivos e imágenes en este momento', + 'attachment.directoryDesktopOnly': 'Las carpetas sólo se pueden agregar en la aplicación de escritorio; agregar archivos individuales en el navegador', + 'attachment.pathUnavailable': 'No se pudo obtener la ruta de la carpeta; arrástralo de nuevo', + 'attachment.pathUnsupported': 'La ruta contiene caracteres que una referencia no puede contener; cámbiale el nombre y vuelve a intentarlo.', + 'image.pending': 'Imágenes pendientes', + 'image.openOriginal': 'Ver original', + 'image.openOriginalLabel': '{label}, haga clic para ver el original', + 'image.remove': 'Eliminar imagen {name}', + 'image.original': 'imagen original', + 'image.label': 'Imagen', + 'image.loadFailed': 'La imagen no se pudo cargar; haga clic para volver a intentar', + 'image.loading': 'Cargando imagen…', + 'image.preview': 'Vista previa de la imagen original', + 'image.closePreview': 'Cerrar vista previa de la imagen original', + 'image.unsupportedType': 'Solo se admiten imágenes PNG, JPG, WebP y GIF', + 'image.tooMany': 'Un mensaje puede incluir hasta imágenes {count}.', + 'image.fileTooLarge': 'Cada imagen debe ser más pequeña que {size}.', + 'image.totalTooLarge': 'Las imágenes superan {size} en total; elimina algunos y vuelve a intentarlo', + 'image.tooManyPixels': 'La resolución de la imagen es demasiado alta; comprímelo y vuelve a intentarlo.', + 'image.dimensionTooLarge': 'Los lados de la imagen deben ser como máximo {size}px; reduce la escala y vuelve a intentarlo', + 'image.modelUnsupported': 'El modelo actual no admite imágenes; cambiar a un modelo que lo haga', + 'image.sendFailed': 'Error al enviar imágenes ({reason}); Vuelve a agregarlos y vuelve a intentarlo.', + 'file.pending': 'Archivos pendientes', + 'file.remove': 'Eliminar archivo {name}', + 'file.uploading': 'Subiendo…', + 'file.uploadFailed': 'La carga falló; haga clic para volver a intentar', + 'file.retry': 'Reintente cargar {name}', + 'file.stillUploading': 'Los archivos todavía se están cargando; enviar después de que terminen', + 'file.sessionUnavailable': 'Sesión no disponible; los archivos no se pueden cargar', + 'file.notStaged': 'El archivo no ha terminado de cargarse; vuelve a agregarlo y vuelve a intentarlo', + 'file.label': 'Archivo', + 'context.aria': '{percent} de contexto utilizado', + 'context.used': 'de contexto utilizado', + 'context.system': 'Aviso del sistema', + 'context.tools': 'Definiciones de herramientas', + 'context.messages': 'Mensajes', + 'settings.enter.title': 'Enviar comportamiento mientras está ocupado', + 'settings.enter.description': 'Qué hacen los botones Intro y Enviar mientras el agente se está ejecutando; Cmd/Ctrl+Enter usa el otro comportamiento', + 'settings.enter.queue': 'cola', + 'settings.enter.steer': 'Redirigir', + 'hero.headline': 'Hacia lo desconocido', + 'hero.preview': 'Vista previa', + 'hero.chooseWorkspace': 'Elige espacio de trabajo', + 'session.hierarchy': 'Jerarquía de sesiones', + 'session.moreViews': 'Más vistas de conversación', + 'todo.title': 'Tareas pendientes', + 'todo.progress.done': '{done} completado', + 'todo.progress.active': '{active} en progreso', + 'todo.progress.pending': '{pending} pendiente', + 'todo.status.completed': 'Completado', + 'todo.status.inProgress': 'En progreso', + 'todo.status.pending': 'Pendiente', + 'todo.rowTitle': 'Actualizar lista de tareas pendientes', + 'tool.title.createGoal': 'Crear objetivo', + 'tool.title.getGoal': 'Ver objetivo', + 'tool.title.updateGoal': 'Actualizar objetivo', + 'tool.preparing.content': '{kilobytes}KB', + 'tool.title.createSchedule': 'Crear recordatorio', + 'tool.title.listSchedules': 'Recordatorios de lista', + 'tool.title.deleteSchedule': 'Eliminar recordatorio', + 'tool.title.updateSchedule': 'Recordatorio de actualización', + 'detail.state': 'Estado', + 'detail.todo.completed': 'Completado', + 'detail.todo.in_progress': 'En progreso', + 'detail.todo.pending': 'Pendiente', + 'detail.todo.empty': 'La lista de tareas pendientes está vacía', + 'todo.diff.initial': 'lista inicial', + 'todo.diff.compare': 'Cambios desde la lista anterior', + 'todo.diff.unavailable': 'Lista anterior no disponible', + 'todo.diff.noChanges': 'No hay cambios en la lista.', + 'todo.diff.added': '{count} añadido', + 'todo.diff.updated': '{count} actualizado', + 'todo.diff.removed': '{count} eliminado', + 'todo.diff.unchanged': '{count} sin cambios', + 'todo.diff.addedItem': 'Añadido', + 'todo.diff.updatedItem': 'Estado cambiado', + 'todo.diff.movedItem': 'Reordenado', + 'todo.diff.removedItem': 'Eliminado', + 'detail.goal.empty': 'Sin gol', + 'detail.goal.active': 'Activo', + 'detail.goal.disarmed': 'En espera de continuación', + 'detail.goal.paused': 'En pausa', + 'detail.goal.blocked': 'Bloqueado', + 'detail.goal.complete': 'Completado', + 'detail.goal.rounds': 'Rondas', + 'detail.goal.reason': 'bloqueador', + 'detail.days': '{count} d', + 'detail.hours': '{count} h', + 'detail.minutes': '{count} mín.', + 'detail.seconds': '{count}', + 'detail.schedule.once': 'una vez', + 'detail.schedule.every': 'Cada {interval}', + 'detail.schedule.when': 'Programado para', + 'detail.schedule.frequency': 'repetir', + 'detail.schedule.scheduled': 'Programado', + 'detail.schedule.overdue': 'Atrasado, a la espera de reanudar la sesión.', + 'detail.schedule.empty': 'Sin recordatorios', + 'detail.schedule.deleted': 'Eliminado', + 'detail.schedule.count': 'Recordatorios de {count}', + 'detail.schedule.daily': 'Todos los días en {time} ({zone})', + 'detail.schedule.weekly': 'Semanalmente en {days} en {time} ({zone})', + 'detail.schedule.cron': 'Cron {expression} ({zone})', + 'detail.weekday.1': 'lun', + 'detail.weekday.2': 'mar', + 'detail.weekday.3': 'mié', + 'detail.weekday.4': 'jueves', + 'detail.weekday.5': 'viernes', + 'detail.weekday.6': 'sábado', + 'detail.weekday.7': 'sol', + 'detail.weekday.join': ', ', + 'tool.title.inspectProviders': 'Inspeccionar proveedores', + 'tool.title.queryRuntime': 'Tiempo de ejecución de consultas', + 'tool.title.inspectPlugins': 'Inspeccionar complementos', + 'tool.title.workflow': 'Ejecutar flujo de trabajo', + 'tool.title.ralph': 'Ejecutar bucle ralph', + 'tool.title.readEvent': 'Leer evento', + 'tool.title.searchEvents': 'Buscar eventos', + 'tool.title.traceEvent': 'Evento de seguimiento', + 'tool.title.searchSessions': 'Buscar sesiones', + 'tool.title.traceSession': 'Sesión de seguimiento', + 'tool.title.listModels': 'Listar modelos', + 'tool.title.subagent': 'Crear subagente', + 'tool.title.listAgents': 'Listar subagentes', + 'tool.title.sendMessage': 'Enviar mensaje', + 'tool.title.interruptAgent': 'agente de interrupción', + 'tool.title.listJobs': 'Listar trabajos en segundo plano', + 'tool.title.readJob': 'Leer la salida del trabajo', + 'tool.title.killJob': 'Cancelar trabajo en segundo plano', + 'tool.title.openTerminal': 'terminal abierta', + 'tool.title.readTerminal': 'Leer terminal', + 'tool.title.listTerminals': 'Lista de terminales', + 'tool.title.signalTerminal': 'terminal de señal', + 'tool.title.closeTerminal': 'Cerrar terminal', + 'tool.title.lsp': 'Símbolos de código de consulta', + 'tool.title.findDefinition': 'encontrar definición', + 'tool.title.findReferences': 'encontrar referencias', + 'tool.title.findImplementation': 'Encontrar implementación', + 'tool.title.hoverSymbol': 'Símbolo de inspección', + 'tool.title.spawnTeammate': 'Crear compañero de equipo', + 'tool.title.createTeamTask': 'Crear tarea de equipo', + 'tool.title.getTeamTask': 'Leer tarea de equipo', + 'tool.title.updateTeamTask': 'Actualizar tarea de equipo', + 'tool.title.listTeamTasks': 'Listar tareas del equipo', + 'tool.title.waitAgent': 'Espere al subagente', + 'detail.recordedResult': 'Resultado registrado', + 'detail.empty': 'Sin resultados', + 'detail.none': 'Ninguno', + 'detail.yes': 'si', + 'detail.no': 'No', + 'detail.moreInInspect': '{count} más artículos disponibles en Inspeccionar', + 'detail.status.running': 'corriendo', + 'detail.status.idle': 'inactivo', + 'detail.status.ready': 'Listo', + 'detail.status.inactive': 'Inactivo', + 'detail.status.provisioning': 'Aprovisionamiento', + 'detail.status.failed': 'Fallido', + 'detail.status.completed': 'Completado', + 'detail.status.deleted': 'Eliminado', + 'detail.status.killed': 'Cancelado', + 'detail.status.accepted': 'Aceptado', + 'detail.status.queued': 'En cola', + 'detail.status.exited': 'Salido', + 'detail.field.id': 'ID', + 'detail.field.revision': 'Revisión', + 'detail.field.platform': 'Plataforma', + 'detail.field.provider': 'Proveedor', + 'detail.field.model': 'modelo', + 'detail.field.role': 'Rol', + 'detail.field.context': 'Contexto', + 'detail.field.owner': 'propietario', + 'detail.field.ready': 'Listo', + 'detail.field.dependencies': 'Dependencias', + 'detail.field.writeScopes': 'Escribir alcances', + 'detail.field.warnings': 'Advertencias', + 'detail.field.diagnostics': 'Diagnóstico', + 'detail.field.methods': 'Métodos', + 'detail.field.inputSchema': 'Esquema de entrada', + 'detail.field.outputSchema': 'Esquema de salida', + 'detail.field.currentPackage': 'Paquete actual', + 'detail.field.nextPackage': 'Siguiente paquete', + 'detail.field.latestRun': 'Última ejecución', + 'detail.field.packages': 'Paquetes', + 'detail.field.registrations': 'Inscripciones', + 'detail.field.props': 'Accesorios', + 'detail.field.data': 'Datos', + 'detail.field.source': 'Fuente', + 'detail.field.content': 'Contenido', + 'detail.field.message': 'Mensaje', + 'detail.field.messageId': 'ID de mensaje', + 'detail.field.root': 'raíz', + 'detail.field.pid': 'ID de proceso', + 'detail.field.type': 'Tipo', + 'detail.field.time': 'tiempo', + 'detail.field.seq': 'Secuencia de eventos', + 'detail.field.turn': 'Turno', + 'detail.field.step': 'paso', + 'detail.field.callId': 'ID de llamada', + 'detail.field.agents': 'Los agentes comenzaron', + 'detail.field.result': 'Resultado', + 'detail.field.parent': 'Padre', + 'detail.field.depth': 'Profundidad', + 'detail.field.exitCode': 'código de salida', + 'detail.field.signal': 'señal', + 'detail.field.previousStatus': 'Estado anterior', + 'detail.field.agent': 'ID del agente', + 'detail.field.job': 'ID de trabajo', + 'detail.field.task': 'Tarea', + 'detail.field.processGroup': 'grupo de procesos', + 'detail.field.availability': 'Disponibilidad', + 'detail.field.bestMatch': 'mejor partido', + 'detail.field.target': 'Evento objetivo', + 'detail.field.surface': 'Estado de registro', + 'detail.agents.count': 'Agentes {count}', + 'detail.jobs.count': 'Trabajos en segundo plano {count}', + 'detail.terminals.count': 'Terminales {count}', + 'detail.tasks.count': 'Tareas del equipo {count}', + 'detail.tasks.nextPage': 'Más tareas disponibles; el siguiente cursor es {cursor}', + 'detail.locations.count': 'Ubicaciones de {count}', + 'detail.location': 'Línea {line}, columna {column}', + 'detail.receipt.delivered': 'Mensaje entregado', + 'detail.receipt.interrupt': 'Interrupción solicitada', + 'detail.receipt.started': 'iniciado', + 'detail.receipt.cancel': 'Cancelación solicitada', + 'detail.receipt.alreadyFinished': 'ya terminé', + 'detail.receipt.signal': 'Señal entregada', + 'detail.receipt.closed': 'Cerrado', + 'detail.receipt.closing': 'Cierre', + 'detail.wait.noProgress': 'No hay subagentes activos', + 'detail.wait.title': 'Actividad del subagente', + 'detail.wait.timeout': 'Se agotó el tiempo de espera', + 'detail.wait.changed': 'Cambio detectado', + 'detail.agent.reply': 'Respuesta del agente', + 'detail.models.title': 'Modelos disponibles', + 'detail.output.lines': 'Líneas {begin}–{end} de {total}', + 'detail.output.truncated': 'Salida truncada', + 'detail.providers.count': '{count} inspeccionar proveedores', + 'detail.plugins.count': 'Complementos dinámicos {count}', + 'detail.workflow.script': 'Guión de flujo de trabajo', + 'detail.ralph.reportedComplete': 'finalización reportada por el trabajador', + 'detail.ralph.reportedBlocker': 'Trabajador reportó un bloqueador', + 'detail.ralph.limit': 'Límite de ronda alcanzado', + 'detail.report.nextSteps': 'Trabajo restante', + 'detail.trace.replacedBy': 'Reemplazado por', + 'detail.trace.replacementChain': 'Cadena de repuesto', + 'detail.trace.replaces': 'Eventos reemplazados', + 'detail.trace.sources': 'Eventos de origen', + 'detail.trace.derived': 'Eventos derivados', + 'detail.trace.ancestors': 'Sesiones de antepasados', + 'detail.trace.descendants': 'Sesiones de descendientes', + 'detail.matches.count': 'Partidos {count}', + 'detail.matches.capped': 'Límite de resultados alcanzado; acotar la búsqueda para más', + 'detail.event.neighbors': 'Eventos circundantes', + 'todo.completed': '{done}/{total} completado', + 'command.attachmentsUnsupported': '/{command} no acepta archivos adjuntos; eliminarlos primero', + 'ask.rowTitle': 'hacer pregunta', + 'ask.waiting': 'esperando', + 'ask.pending': 'continuó; respuesta aún disponible', + 'ask.pendingDetail': 'Estas cuestiones pendientes siguen siendo responsabilidad del compositor.', + 'ask.reopen': 'respuesta', + 'ask.review': 'Ver respuestas', + 'ask.closed': 'cerrado', + 'ask.closedDetail': 'Esta pregunta está cerrada; su resultado está en la conversación a continuación.', + 'ask.cancelled': 'cancelado', + 'ask.cancelledDetail': 'Este conjunto de preguntas se canceló antes de enviar las respuestas.', + 'ask.interrupted': 'interrumpido', + 'ask.interruptedDetail': 'Este conjunto de preguntas se interrumpió antes de que se enviaran las respuestas.', + 'ask.answered': '{answered}/{total} respondió', + 'ask.skipped': 'No respondido', + 'bash.running': 'corriendo', + 'bash.failed': 'Fallido', + 'bash.stopped': 'Detenido', + 'row.running': 'corriendo', + 'row.preparing': 'Preparando la llamada a la herramienta', + 'row.failed': 'Fallido', + 'row.stopped': 'Detenido', + 'row.commandArguments': 'Argumentos de comando', + 'row.input': 'IN', + 'row.output': 'OUT', + 'row.inspect': 'inspeccionar', + 'row.imageSize': '{width}x{height}px, {bytes} bytes', + 'row.imageSizeDownscaled': '{width}x{height}px (reducido de {originalWidth}x{originalHeight}px), {bytes} bytes', + 'tool.title.search': 'Buscar', + 'tool.title.read': 'leer', + 'tool.title.bash': 'golpe', + 'tool.title.write': 'escribir', + 'tool.title.edit': 'Editar', + 'tool.title.code': 'Código', + 'tool.title.generic': 'llamada de herramienta', + 'tool.title.inspect': 'Consulta entorno Cordis', + 'tool.title.runCordis': 'Ejecute el complemento Cordis', + 'tool.title.stopCordis': 'Detener el complemento Cordis', + 'tool.title.removeCordis': 'Eliminar el complemento Cordis', + 'tool.title.pwsh': 'Pwsh', + 'tool.title.readImage': 'Leer imagen', + 'tool.title.grep': 'grep', + 'tool.title.glob': 'globo', + 'tool.title.webSearch': 'Buscar', + 'tool.title.webFetch': 'buscar', + 'tool.autoReviewRejected': 'Rechazado por Auto review', + 'tool.autoReviewNotExecuted': 'La herramienta no fue ejecutada. Razón: {reason}', + 'tool.autoReviewReasonFallback': 'Auto review no autorizó esta acción', + 'diff.collapseAria': 'Contraer diferencia', + 'diff.expandAria': 'Expanda {count} más líneas diferenciales', + 'diff.expandRest': '… {count} más líneas', + 'read.window': 'Mostrando {shown} de líneas {total}', + 'read.collapseAria': 'Contraer contenido', + 'read.expandAria': 'Ampliar {count} más líneas', + 'read.expandRest': '… {count} más líneas', + 'search.paths': 'Rutas {shown}', + 'search.paths.truncated': 'Mostrando {shown} de las rutas {total}', + 'search.matches': 'Coincidencias {shown} · Archivos {files}', + 'search.matches.truncated': 'Mostrando {shown} de coincidencias {total} · Archivos {files}', + 'search.noResults': 'Sin resultados', + 'search.collapseAria': 'Contraer resultados', + 'search.expandAria': 'Expandir {count} más líneas de resultados', + 'search.expandRest': '… {count} más líneas', + 'web.noResults': 'No se encontraron resultados', + 'web.sourcesTruncated': 'Lista de fuentes truncada', + 'web.http': 'HTTP', + 'web.contentTruncated': 'Contenido truncado', + 'details.running': 'Corriendo…', + 'queue.count': 'Mensajes en cola {n}', + 'queue.sending': 'Enviando…', + 'queue.image': 'Imagen de mensaje en cola', + 'queue.file': 'Archivo en cola {name}', + 'queue.edit': 'Editar mensaje en cola', + 'queue.edit.unsupported': 'Contiene contenido que no es texto; la edición aún no es compatible', + 'queue.save': 'Guardar mensaje en cola', + 'queue.cancelEdit': 'Cancelar edición', + 'queue.remove': 'Eliminar mensaje en cola', + 'queue.steer': 'Redirigir mensaje en cola', + 'queue.steer.unavailable': 'La dirección solo está disponible mientras el agente está en ejecución', + 'error.sessionInUse': 'Esta sesión ya está en uso, posiblemente por otra instancia de DSH en ejecución (como la web dsh o la aplicación de escritorio). Salga de otras instancias de DSH en ejecución y vuelva a intentarlo.', + 'queue.editFailed': 'Error de edición: es posible que este mensaje ya haya comenzado a enviarse.', + 'queue.removeFailed': 'Error al eliminar: es posible que este mensaje ya haya comenzado a enviarse.', + 'queue.steerFailed': 'La dirección falló. Intentar otra vez.', + 'terminal.signal': 'señal {signal}', + 'terminal.exitCode': 'código de salida {code}', + 'terminal.noExitCode': 'sin código de salida', + 'terminal.commandLine': 'Línea de comando {n}', + 'terminal.running': 'corriendo', + 'terminal.failed': 'Fallido', + 'terminal.done': 'hecho', + 'terminal.noOutput': 'Sin salida', + 'terminal.collapseAria': 'Contraer salida', + 'terminal.expandAria': 'Expanda las líneas de salida {n} restantes', + 'terminal.expandRest': '… {n} más líneas', + // The Host terminal_send presenter has no locale seat; keep its fallbacks + // aligned with these English values. + 'terminal.sendInput': '(enviar entrada)', + 'terminal.session': 'Terminal {sessionId}', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'quote.remove': 'Supprimer le devis', + 'shortcut.newline': 'Nouvelle ligne', + 'shortcut.complementary': 'Utiliser l’action alternative de mise en attente ou de guidage', + 'shortcut.slash': 'Ouvrir le menu des commandes', + 'shortcut.mention': 'Ouvrir le menu de référence', + 'hint.plan': PLAN_NEXT_ACTION_EN, + 'hint.goal': "décrire l'objectif d'une tâche de longue durée", + 'hint.goal.active': 'objectif actif — modifier/mettre en pause/reprendre/effacer', + 'placeholder.plan': PLAN_NEXT_ACTION_EN, + 'placeholder.default': 'Envoyez un message ou exécutez une tâche, des commandes /, des fichiers @ ou des sessions', + 'placeholder.unavailable': 'Séance indisponible', + 'placeholder.parentOffline': "Session parent hors ligne ; l'envoi n'est pas disponible mais vous pouvez toujours arrêter l'exécution", + 'placeholder.hero': 'Décrivez ce que vous souhaitez construire, / commandes, @ fichiers ou sessions', + 'placeholder.workspace': 'Choisissez un espace de travail pour commencer', + 'placeholder.steerQueue': "Cmd/Ctrl+Entrée dirige tous les messages en file d'attente", + 'input.commands': 'Ajouter des fichiers ou exécuter des commandes', + 'input.file': 'Fichier', + 'input.stop': 'Arrêter la génération', + 'input.send': 'Envoyer le message', + 'input.send.queue': "Message de file d'attente", + 'input.send.steer': 'Guider avec ce message', + 'attachment.pending': 'Pièces jointes en attente', + 'attachment.scrollLeft': 'Faire défiler les pièces jointes vers la gauche', + 'attachment.scrollRight': 'Faire défiler les pièces jointes vers la droite', + 'attachment.dropTitle': 'Faites glisser des fichiers ou des images ici pour les ajouter', + 'attachment.dropDesc': "Limite d'images : jusqu'à des images {count}, {size} chacune", + 'attachment.dropBlocked': 'Les fichiers et les images ne peuvent pas être ajoutés pour le moment', + 'attachment.directoryDesktopOnly': "Les dossiers ne peuvent être ajoutés que dans l'application de bureau ; ajouter des fichiers individuels dans le navigateur", + 'attachment.pathUnavailable': "Impossible d'obtenir le chemin du dossier ; fais-le glisser à nouveau", + 'attachment.pathUnsupported': "Le chemin contient des caractères qu'une référence ne peut pas transporter ; renommez-le et réessayez", + 'image.pending': 'Images en attente', + 'image.openOriginal': "Voir l'original", + 'image.openOriginalLabel': "{label}, cliquez pour voir l'original", + 'image.remove': "Supprimer l'image {name}", + 'image.original': 'Image originale', + 'image.label': 'Images', + 'image.loadFailed': "L'image n'a pas pu être chargée ; cliquez pour réessayer", + 'image.loading': "Chargement de l'image…", + 'image.preview': "Aperçu de l'image originale", + 'image.closePreview': "Fermer l'aperçu de l'image d'origine", + 'image.unsupportedType': 'Seules les images PNG, JPG, WebP et GIF sont prises en charge', + 'image.tooMany': "Un message peut inclure jusqu'à des images {count}", + 'image.fileTooLarge': 'Chaque image doit être plus petite que {size}', + 'image.totalTooLarge': 'Les images dépassent {size} au total ; enlève-en un peu et réessaye', + 'image.tooManyPixels': "La résolution de l'image est trop élevée ; compressez-le et réessayez", + 'image.dimensionTooLarge': "Les côtés de l'image doivent être au maximum {size}px ; réduisez-le et réessayez", + 'image.modelUnsupported': 'Le modèle actuel ne prend pas en charge les images ; passer à un modèle qui le fait', + 'image.sendFailed': "L'envoi des images a échoué ({reason}) ; rajoutez-les et réessayez", + 'file.pending': 'Fichiers en attente', + 'file.remove': 'Supprimer le fichier {name}', + 'file.uploading': 'Téléchargement…', + 'file.uploadFailed': 'Échec du téléchargement ; cliquez pour réessayer', + 'file.retry': 'Réessayez de télécharger {name}', + 'file.stillUploading': 'Les fichiers sont toujours en cours de téléchargement ; envoyer après avoir fini', + 'file.sessionUnavailable': 'Séance indisponible ; les fichiers ne peuvent pas être téléchargés', + 'file.notStaged': "Le téléchargement du fichier n'est pas terminé ; rajoutez-le et réessayez", + 'file.label': 'Fichier', + 'context.aria': '{percent} de contexte utilisé', + 'context.used': 'du contexte utilisé', + 'context.system': 'Invite système', + 'context.tools': "Définitions d'outils", + 'context.messages': 'Messages', + 'settings.enter.title': "Comportement d'envoi lorsque vous êtes occupé", + 'settings.enter.description': "Ce que font Entrée et le bouton Envoyer pendant l'exécution de l'agent ; Cmd/Ctrl+Entrée utilise l'autre comportement", + 'settings.enter.queue': "File d'attente", + 'settings.enter.steer': 'Guider', + 'hero.headline': "Dans l'inconnu", + 'hero.preview': 'Aperçu', + 'hero.chooseWorkspace': 'Choisir un espace de travail', + 'session.hierarchy': 'Hiérarchie des sessions', + 'session.moreViews': 'Plus de vues de conversations', + 'todo.title': 'À faire', + 'todo.progress.done': '{done} terminé', + 'todo.progress.active': '{active} en cours', + 'todo.progress.pending': '{pending} en attente', + 'todo.status.completed': 'Terminé', + 'todo.status.inProgress': 'En cours', + 'todo.status.pending': 'En attente', + 'todo.rowTitle': 'Mettre à jour la liste de tâches', + 'tool.title.createGoal': 'Créer un objectif', + 'tool.title.getGoal': "Afficher l'objectif", + 'tool.title.updateGoal': 'Objectif de mise à jour', + 'tool.preparing.content': '{kilobytes}KB', + 'tool.title.createSchedule': 'Créer un rappel', + 'tool.title.listSchedules': 'Liste des rappels', + 'tool.title.deleteSchedule': 'Supprimer le rappel', + 'tool.title.updateSchedule': 'Rappel de mise à jour', + 'detail.state': 'Statut', + 'detail.todo.completed': 'Terminé', + 'detail.todo.in_progress': 'En cours', + 'detail.todo.pending': 'En attente', + 'detail.todo.empty': 'La liste de tâches est vide', + 'todo.diff.initial': 'Liste initiale', + 'todo.diff.compare': 'Changements depuis la liste précédente', + 'todo.diff.unavailable': 'Liste précédente indisponible', + 'todo.diff.noChanges': 'Aucun changement dans la liste', + 'todo.diff.added': '{count} ajouté', + 'todo.diff.updated': '{count} mis à jour', + 'todo.diff.removed': '{count} supprimé', + 'todo.diff.unchanged': '{count} inchangé', + 'todo.diff.addedItem': 'Ajouté', + 'todo.diff.updatedItem': 'Statut modifié', + 'todo.diff.movedItem': 'Réorganisé', + 'todo.diff.removedItem': 'Supprimé', + 'detail.goal.empty': 'Aucun but', + 'detail.goal.active': 'Actif', + 'detail.goal.disarmed': 'En attente de suite', + 'detail.goal.paused': 'En pause', + 'detail.goal.blocked': 'Bloqué', + 'detail.goal.complete': 'Terminé', + 'detail.goal.rounds': 'Tours', + 'detail.goal.reason': 'Bloqueur', + 'detail.days': '{count}d', + 'detail.hours': '{count} h', + 'detail.minutes': '{count} min.', + 'detail.seconds': '{count}s', + 'detail.schedule.once': 'Une fois', + 'detail.schedule.every': 'Chaque {interval}', + 'detail.schedule.when': 'Prévu pour', + 'detail.schedule.frequency': 'Répéter', + 'detail.schedule.scheduled': 'Programmé', + 'detail.schedule.overdue': 'En retard, en attente de reprise de la session', + 'detail.schedule.empty': 'Aucun rappel', + 'detail.schedule.deleted': 'Supprimé', + 'detail.schedule.count': 'Rappels {count}', + 'detail.schedule.daily': 'Tous les jours à {time} ({zone})', + 'detail.schedule.weekly': 'Hebdomadaire sur {days} à {time} ({zone})', + 'detail.schedule.cron': 'Cron {expression} ({zone})', + 'detail.weekday.1': 'lundi', + 'detail.weekday.2': 'mar.', + 'detail.weekday.3': 'mer', + 'detail.weekday.4': 'jeu.', + 'detail.weekday.5': 'vendredi', + 'detail.weekday.6': 'Samedi', + 'detail.weekday.7': 'Soleil', + 'detail.weekday.join': ', ', + 'tool.title.inspectProviders': 'Inspecter les fournisseurs', + 'tool.title.queryRuntime': 'Exécution de la requête', + 'tool.title.inspectPlugins': 'Inspecter les plugins', + 'tool.title.workflow': 'Exécuter le flux de travail', + 'tool.title.ralph': 'Exécuter la boucle Ralph', + 'tool.title.readEvent': "Lire l'événement", + 'tool.title.searchEvents': 'Rechercher des événements', + 'tool.title.traceEvent': 'Événement de trace', + 'tool.title.searchSessions': 'Rechercher des sessions', + 'tool.title.traceSession': 'Session de traçage', + 'tool.title.listModels': 'Liste des modèles', + 'tool.title.subagent': 'Créer un sous-agent', + 'tool.title.listAgents': 'Liste des sous-agents', + 'tool.title.sendMessage': 'Envoyer le message', + 'tool.title.interruptAgent': "Agent d'interruption", + 'tool.title.listJobs': 'Répertorier les tâches en arrière-plan', + 'tool.title.readJob': 'Lire le résultat de la tâche', + 'tool.title.killJob': 'Annuler le travail en arrière-plan', + 'tool.title.openTerminal': 'Terminal ouvert', + 'tool.title.readTerminal': 'Lire le terminal', + 'tool.title.listTerminals': 'Liste des terminaux', + 'tool.title.signalTerminal': 'Borne de signal', + 'tool.title.closeTerminal': 'Fermer le terminal', + 'tool.title.lsp': 'Symboles du code de requête', + 'tool.title.findDefinition': 'Trouver une définition', + 'tool.title.findReferences': 'Trouver des références', + 'tool.title.findImplementation': 'Trouver la mise en œuvre', + 'tool.title.hoverSymbol': 'Inspecter le symbole', + 'tool.title.spawnTeammate': 'Créer un coéquipier', + 'tool.title.createTeamTask': "Créer une tâche d'équipe", + 'tool.title.getTeamTask': "Lire la tâche d'équipe", + 'tool.title.updateTeamTask': "Mettre à jour la tâche de l'équipe", + 'tool.title.listTeamTasks': "Répertorier les tâches de l'équipe", + 'tool.title.waitAgent': 'Attendez le sous-agent', + 'detail.recordedResult': 'Résultat enregistré', + 'detail.empty': 'Aucun résultat', + 'detail.none': 'Aucun', + 'detail.yes': 'Oui', + 'detail.no': 'Non', + 'detail.moreInInspect': "{count} plus d'articles disponibles dans Inspecter", + 'detail.status.running': 'Courir', + 'detail.status.idle': 'Inactif', + 'detail.status.ready': 'Prêt', + 'detail.status.inactive': 'Inactif', + 'detail.status.provisioning': 'Approvisionnement', + 'detail.status.failed': 'Échec', + 'detail.status.completed': 'Terminé', + 'detail.status.deleted': 'Supprimé', + 'detail.status.killed': 'Annulé', + 'detail.status.accepted': 'Accepté', + 'detail.status.queued': "En file d'attente", + 'detail.status.exited': 'Quitté', + 'detail.field.id': 'ID', + 'detail.field.revision': 'Révision', + 'detail.field.platform': 'Plateforme', + 'detail.field.provider': 'Fournisseur', + 'detail.field.model': 'Modèle', + 'detail.field.role': 'Rôle', + 'detail.field.context': 'Contexte', + 'detail.field.owner': 'Propriétaire', + 'detail.field.ready': 'Prêt', + 'detail.field.dependencies': 'Dépendances', + 'detail.field.writeScopes': 'Écrire des étendues', + 'detail.field.warnings': 'Avertissements', + 'detail.field.diagnostics': 'Diagnostic', + 'detail.field.methods': 'Méthodes', + 'detail.field.inputSchema': "Schéma d'entrée", + 'detail.field.outputSchema': 'Schéma de sortie', + 'detail.field.currentPackage': 'Forfait actuel', + 'detail.field.nextPackage': 'Forfait suivant', + 'detail.field.latestRun': 'Dernière course', + 'detail.field.packages': 'Forfaits', + 'detail.field.registrations': 'Inscriptions', + 'detail.field.props': 'Accessoires', + 'detail.field.data': 'Données', + 'detail.field.source': 'Source', + 'detail.field.content': 'Contenu', + 'detail.field.message': 'Message', + 'detail.field.messageId': 'Identifiant du message', + 'detail.field.root': 'Racine', + 'detail.field.pid': 'ID de processus', + 'detail.field.type': 'Tapez', + 'detail.field.time': 'Temps', + 'detail.field.seq': "Séquence d'événements", + 'detail.field.turn': 'Tour', + 'detail.field.step': 'Étape', + 'detail.field.callId': "Identifiant d'appel", + 'detail.field.agents': 'Les agents ont commencé', + 'detail.field.result': 'Résultat', + 'detail.field.parent': 'Parent', + 'detail.field.depth': 'Profondeur', + 'detail.field.exitCode': 'Code de sortie', + 'detail.field.signal': 'Signal', + 'detail.field.previousStatus': 'Statut précédent', + 'detail.field.agent': "ID d'agent", + 'detail.field.job': 'ID de travail', + 'detail.field.task': 'Tâche', + 'detail.field.processGroup': 'Groupe de processus', + 'detail.field.availability': 'Disponibilité', + 'detail.field.bestMatch': 'Meilleure correspondance', + 'detail.field.target': 'Événement cible', + 'detail.field.surface': "Statut d'enregistrement", + 'detail.agents.count': 'Agents {count}', + 'detail.jobs.count': "Travaux d'arrière-plan {count}", + 'detail.terminals.count': 'Bornes {count}', + 'detail.tasks.count': "Tâches de l'équipe {count}", + 'detail.tasks.nextPage': 'Plus de tâches disponibles ; le curseur suivant est {cursor}', + 'detail.locations.count': 'Localisations {count}', + 'detail.location': 'Ligne {line}, colonne {column}', + 'detail.receipt.delivered': 'Message délivré', + 'detail.receipt.interrupt': 'Interruption demandée', + 'detail.receipt.started': 'Commencé', + 'detail.receipt.cancel': 'Annulation demandée', + 'detail.receipt.alreadyFinished': 'Déjà fini', + 'detail.receipt.signal': 'Signal délivré', + 'detail.receipt.closed': 'Fermé', + 'detail.receipt.closing': 'Clôture', + 'detail.wait.noProgress': 'Aucun sous-agent actif', + 'detail.wait.title': 'Activité du sous-agent', + 'detail.wait.timeout': "Le délai d'attente a expiré", + 'detail.wait.changed': 'Changement détecté', + 'detail.agent.reply': "Réponse de l'agent", + 'detail.models.title': 'Modèles disponibles', + 'detail.output.lines': 'Lignes {begin}–{end} de {total}', + 'detail.output.truncated': 'Sortie tronquée', + 'detail.providers.count': '{count} inspecte les fournisseurs', + 'detail.plugins.count': 'Plugins dynamiques {count}', + 'detail.workflow.script': 'Script de flux de travail', + 'detail.ralph.reportedComplete': "Le travailleur a signalé l'achèvement", + 'detail.ralph.reportedBlocker': 'Le travailleur a signalé un bloqueur', + 'detail.ralph.limit': 'Limite de tours atteinte', + 'detail.report.nextSteps': 'Travail restant', + 'detail.trace.replacedBy': 'Remplacé par', + 'detail.trace.replacementChain': 'Chaîne de remplacement', + 'detail.trace.replaces': 'Événements remplacés', + 'detail.trace.sources': 'Événements sources', + 'detail.trace.derived': 'Événements dérivés', + 'detail.trace.ancestors': "Séances d'ancêtres", + 'detail.trace.descendants': 'Séances descendantes', + 'detail.matches.count': 'Correspondances {count}', + 'detail.matches.capped': 'Limite de résultat atteinte ; affiner la recherche pour en savoir plus', + 'detail.event.neighbors': 'Événements environnants', + 'todo.completed': '{done}/{total} terminé', + 'command.attachmentsUnsupported': "/{command} n'accepte pas les pièces jointes ; retirez-les d'abord", + 'ask.rowTitle': 'Poser une question', + 'ask.waiting': 'en attendant', + 'ask.pending': 'suite ; réponse toujours disponible', + 'ask.pendingDetail': 'Ces questions en suspens restent à la charge du compositeur.', + 'ask.reopen': 'Réponse', + 'ask.review': 'Afficher les réponses', + 'ask.closed': 'fermé', + 'ask.closedDetail': 'Cette question est fermée ; son résultat est dans la conversation ci-dessous.', + 'ask.cancelled': 'annulé', + 'ask.cancelledDetail': 'Cet ensemble de questions a été annulé avant que les réponses ne soient soumises.', + 'ask.interrupted': 'interrompu', + 'ask.interruptedDetail': 'Cette série de questions a été interrompue avant que les réponses ne soient soumises.', + 'ask.answered': '{answered}/{total} répondu', + 'ask.skipped': 'Pas de réponse', + 'bash.running': 'Courir', + 'bash.failed': 'Échec', + 'bash.stopped': 'Arrêté', + 'row.running': 'Courir', + 'row.preparing': "Préparation de l'appel à l'outil", + 'row.failed': 'Échec', + 'row.stopped': 'Arrêté', + 'row.commandArguments': 'Arguments de commande', + 'row.input': 'IN', + 'row.output': 'OUT', + 'row.inspect': 'Inspecter', + 'row.imageSize': '{width}x{height}px, {bytes} octets', + 'row.imageSizeDownscaled': '{width}x{height}px (réduction de {originalWidth}x{originalHeight}px), {bytes} octets', + 'tool.title.search': 'Rechercher', + 'tool.title.read': 'Lire', + 'tool.title.bash': 'Frapper', + 'tool.title.write': 'Écrire', + 'tool.title.edit': 'Modifier', + 'tool.title.code': 'Coder', + 'tool.title.generic': "Appel d'outil", + 'tool.title.inspect': "Interroger l'environnement Cordis", + 'tool.title.runCordis': 'Exécuter le plugin Cordis', + 'tool.title.stopCordis': 'Arrêter le plug-in Cordis', + 'tool.title.removeCordis': 'Supprimer le plug-in Cordis', + 'tool.title.pwsh': 'Pwsh', + 'tool.title.readImage': "Lire l'image", + 'tool.title.grep': 'Grep', + 'tool.title.glob': 'Glob', + 'tool.title.webSearch': 'Rechercher', + 'tool.title.webFetch': 'Récupérer', + 'tool.autoReviewRejected': 'Rejeté par Auto review', + 'tool.autoReviewNotExecuted': "L'outil n'a pas été exécuté. Raison : {reason}", + 'tool.autoReviewReasonFallback': "Auto review n'a pas autorisé cette action", + 'diff.collapseAria': 'Réduire la différence', + 'diff.expandAria': 'Développez {count} plus de lignes de différence', + 'diff.expandRest': '… {count} plus de lignes', + 'read.window': 'Affichage des lignes {shown} des lignes {total}', + 'read.collapseAria': 'Réduire le contenu', + 'read.expandAria': 'Développez {count} plus de lignes', + 'read.expandRest': '… {count} plus de lignes', + 'search.paths': 'Chemins {shown}', + 'search.paths.truncated': 'Affichage des chemins {shown} sur {total}', + 'search.matches': 'Correspondances {shown} · Fichiers {files}', + 'search.matches.truncated': 'Affichage des correspondances {shown} des correspondances {total} · Fichiers {files}', + 'search.noResults': 'Aucun résultat', + 'search.collapseAria': 'Réduire les résultats', + 'search.expandAria': 'Développez {count} plus de lignes de résultats', + 'search.expandRest': '… {count} plus de lignes', + 'web.noResults': 'Aucun résultat trouvé', + 'web.sourcesTruncated': 'Liste source tronquée', + 'web.http': 'HTTP', + 'web.contentTruncated': 'Contenu tronqué', + 'details.running': 'Courir…', + 'queue.count': "Messages en file d'attente {n}", + 'queue.sending': 'Envoi…', + 'queue.image': "Image du message en file d'attente", + 'queue.file': "Fichier en file d'attente {name}", + 'queue.edit': "Modifier le message en file d'attente", + 'queue.edit.unsupported': "Contient du contenu non textuel ; l'édition n'est pas encore prise en charge", + 'queue.save': "Enregistrer le message en file d'attente", + 'queue.cancelEdit': 'Annuler la modification', + 'queue.remove': "Supprimer le message en file d'attente", + 'queue.steer': 'Guider avec le message en attente', + 'queue.steer.unavailable': "Le pilotage est disponible uniquement lorsque l'agent est en cours d'exécution", + 'error.sessionInUse': "Cette session est déjà utilisée, éventuellement par une autre instance DSH en cours d'exécution (telle que le Web dsh ou l'application de bureau). Quittez les autres instances DSH en cours d'exécution et réessayez.", + 'queue.editFailed': "Échec de la modification : l'envoi de ce message a peut-être déjà commencé.", + 'queue.removeFailed': "Échec de la suppression : l'envoi de ce message a peut-être déjà commencé.", + 'queue.steerFailed': 'La direction a échoué. Essayer à nouveau.', + 'terminal.signal': 'signal {signal}', + 'terminal.exitCode': 'code de sortie {code}', + 'terminal.noExitCode': 'pas de code de sortie', + 'terminal.commandLine': 'Ligne de commande {n}', + 'terminal.running': 'Courir', + 'terminal.failed': 'Échec', + 'terminal.done': 'Terminé', + 'terminal.noOutput': 'Aucune sortie', + 'terminal.collapseAria': 'Réduire la sortie', + 'terminal.expandAria': 'Développez les lignes de sortie {n} restantes', + 'terminal.expandRest': '… {n} plus de lignes', + // The Host terminal_send presenter has no locale seat; keep its fallbacks + // aligned with these English values. + 'terminal.sendInput': '(envoyer une entrée)', + 'terminal.session': 'Borne {sessionId}', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'quote.remove': 'Zitat entfernen', + 'shortcut.newline': 'Neue Zeile', + 'shortcut.complementary': 'Die alternative Aktion zum Vormerken oder Lenken verwenden', + 'shortcut.slash': 'Befehlsmenü öffnen', + 'shortcut.mention': 'Referenzmenü öffnen', + 'hint.plan': PLAN_NEXT_ACTION_EN, + 'hint.goal': 'Beschreiben Sie das Ziel einer lang andauernden Aufgabe', + 'hint.goal.active': 'Ziel aktiv – Bearbeiten / Anhalten / Fortsetzen / Löschen', + 'placeholder.plan': PLAN_NEXT_ACTION_EN, + 'placeholder.default': 'Senden Sie eine Nachricht oder führen Sie eine Aufgabe, /-Befehle, @-Dateien oder Sitzungen aus', + 'placeholder.unavailable': 'Sitzung nicht verfügbar', + 'placeholder.parentOffline': 'Elternsitzung offline; Das Senden ist nicht möglich, aber Sie können den Lauf trotzdem stoppen', + 'placeholder.hero': 'Beschreiben Sie, was Sie erstellen möchten: /-Befehle, @-Dateien oder Sitzungen', + 'placeholder.workspace': 'Wählen Sie einen Arbeitsbereich zum Starten aus', + 'placeholder.steerQueue': 'Cmd/Strg+Enter steuert alle Nachrichten in der Warteschlange', + 'input.commands': 'Fügen Sie Dateien hinzu oder führen Sie Befehle aus', + 'input.file': 'Datei', + 'input.stop': 'Generierung stoppen', + 'input.send': 'Nachricht senden', + 'input.send.queue': 'Warteschlangennachricht', + 'input.send.steer': 'Mit Nachricht lenken', + 'attachment.pending': 'Ausstehende Anhänge', + 'attachment.scrollLeft': 'Anhänge nach links scrollen', + 'attachment.scrollRight': 'Anhänge nach rechts scrollen', + 'attachment.dropTitle': 'Ziehen Sie Dateien oder Bilder hierher, um sie hinzuzufügen', + 'attachment.dropDesc': 'Bildbeschränkung: bis zu {count} Bilder, jeweils {size}', + 'attachment.dropBlocked': 'Dateien und Bilder können derzeit nicht hinzugefügt werden', + 'attachment.directoryDesktopOnly': 'Ordner können nur in der Desktop-App hinzugefügt werden; einzelne Dateien im Browser hinzufügen', + 'attachment.pathUnavailable': 'Der Ordnerpfad konnte nicht abgerufen werden; Ziehen Sie es erneut hinein', + 'attachment.pathUnsupported': 'Der Pfad enthält Zeichen, die eine Referenz nicht enthalten kann; Benennen Sie es um und versuchen Sie es erneut', + 'image.pending': 'Ausstehende Bilder', + 'image.openOriginal': 'Original ansehen', + 'image.openOriginalLabel': '{label}, klicken Sie, um das Original anzuzeigen', + 'image.remove': 'Bild {name} entfernen', + 'image.original': 'Originalbild', + 'image.label': 'Bild', + 'image.loadFailed': 'Bild konnte nicht geladen werden; Klicken Sie, um es noch einmal zu versuchen', + 'image.loading': 'Bild wird geladen…', + 'image.preview': 'Originalbildvorschau', + 'image.closePreview': 'Originalbildvorschau schließen', + 'image.unsupportedType': 'Es werden nur PNG-, JPG-, WebP- und GIF-Bilder unterstützt', + 'image.tooMany': 'Eine Nachricht kann bis zu {count} Bilder enthalten', + 'image.fileTooLarge': 'Jedes Bild muss kleiner als {size} sein', + 'image.totalTooLarge': 'Die Anzahl der Bilder übersteigt insgesamt {size}; Entfernen Sie einige davon und versuchen Sie es erneut', + 'image.tooManyPixels': 'Die Bildauflösung ist zu hoch; Komprimieren Sie es und versuchen Sie es erneut', + 'image.dimensionTooLarge': 'Bildseiten dürfen höchstens {size}px sein; Skalieren Sie es herunter und versuchen Sie es erneut', + 'image.modelUnsupported': 'Das aktuelle Modell unterstützt keine Bilder; Wechseln Sie zu einem Modell, das dies tut', + 'image.sendFailed': 'Das Senden von Bildern ist fehlgeschlagen ({reason}); Fügen Sie sie erneut hinzu und versuchen Sie es erneut', + 'file.pending': 'Ausstehende Dateien', + 'file.remove': 'Datei {name} entfernen', + 'file.uploading': 'Hochladen…', + 'file.uploadFailed': 'Hochladen fehlgeschlagen; Klicken Sie, um es noch einmal zu versuchen', + 'file.retry': 'Versuchen Sie erneut, {name} hochzuladen', + 'file.stillUploading': 'Dateien werden noch hochgeladen; senden, nachdem sie fertig sind', + 'file.sessionUnavailable': 'Sitzung nicht verfügbar; Dateien können nicht hochgeladen werden', + 'file.notStaged': 'Der Upload der Datei wurde noch nicht abgeschlossen. Fügen Sie es erneut hinzu und versuchen Sie es erneut', + 'file.label': 'Datei', + 'context.aria': '{percent} des verwendeten Kontexts', + 'context.used': 'des verwendeten Kontexts', + 'context.system': 'Systemaufforderung', + 'context.tools': 'Werkzeugdefinitionen', + 'context.messages': 'Nachrichten', + 'settings.enter.title': 'Sendeverhalten, während beschäftigt ist', + 'settings.enter.description': 'Was die Eingabetaste und die Schaltfläche „Senden“ bewirken, während der Agent ausgeführt wird; Cmd/Strg+Enter verwendet das andere Verhalten', + 'settings.enter.queue': 'Warteschlange', + 'settings.enter.steer': 'Lenken', + 'hero.headline': 'Ins Unbekannte', + 'hero.preview': 'Vorschau', + 'hero.chooseWorkspace': 'Wählen Sie einen Arbeitsbereich', + 'session.hierarchy': 'Sitzungshierarchie', + 'session.moreViews': 'Weitere Konversationsansichten', + 'todo.title': 'Aufgaben', + 'todo.progress.done': '{done} abgeschlossen', + 'todo.progress.active': '{active} in Bearbeitung', + 'todo.progress.pending': '{pending} ausstehend', + 'todo.status.completed': 'Abgeschlossen', + 'todo.status.inProgress': 'In Bearbeitung', + 'todo.status.pending': 'Ausstehend', + 'todo.rowTitle': 'To-Do-Liste aktualisieren', + 'tool.title.createGoal': 'Ziel erstellen', + 'tool.title.getGoal': 'Ziel ansehen', + 'tool.title.updateGoal': 'Ziel aktualisieren', + 'tool.preparing.content': '{kilobytes}KB', + 'tool.title.createSchedule': 'Erinnerung erstellen', + 'tool.title.listSchedules': 'Listen Sie Erinnerungen auf', + 'tool.title.deleteSchedule': 'Erinnerung löschen', + 'tool.title.updateSchedule': 'Update-Erinnerung', + 'detail.state': 'Status', + 'detail.todo.completed': 'Abgeschlossen', + 'detail.todo.in_progress': 'In Bearbeitung', + 'detail.todo.pending': 'Ausstehend', + 'detail.todo.empty': 'Die To-Do-Liste ist leer', + 'todo.diff.initial': 'Erste Liste', + 'todo.diff.compare': 'Änderungen gegenüber der vorherigen Liste', + 'todo.diff.unavailable': 'Vorherige Liste nicht verfügbar', + 'todo.diff.noChanges': 'Keine Änderungen an der Liste', + 'todo.diff.added': '{count} hinzugefügt', + 'todo.diff.updated': '{count} aktualisiert', + 'todo.diff.removed': '{count} entfernt', + 'todo.diff.unchanged': '{count} unverändert', + 'todo.diff.addedItem': 'Hinzugefügt', + 'todo.diff.updatedItem': 'Status geändert', + 'todo.diff.movedItem': 'Nachbestellt', + 'todo.diff.removedItem': 'Entfernt', + 'detail.goal.empty': 'Kein Ziel', + 'detail.goal.active': 'Aktiv', + 'detail.goal.disarmed': 'Warten auf Fortsetzung', + 'detail.goal.paused': 'Angehalten', + 'detail.goal.blocked': 'Blockiert', + 'detail.goal.complete': 'Abgeschlossen', + 'detail.goal.rounds': 'Runden', + 'detail.goal.reason': 'Blocker', + 'detail.days': '{count} d', + 'detail.hours': '{count} h', + 'detail.minutes': '{count} min', + 'detail.seconds': '{count} s', + 'detail.schedule.once': 'Einmal', + 'detail.schedule.every': 'Jeder {interval}', + 'detail.schedule.when': 'Geplant für', + 'detail.schedule.frequency': 'Wiederholen', + 'detail.schedule.scheduled': 'Geplant', + 'detail.schedule.overdue': 'Überfällig, ich warte auf die Fortsetzung der Sitzung', + 'detail.schedule.empty': 'Keine Erinnerungen', + 'detail.schedule.deleted': 'Gelöscht', + 'detail.schedule.count': '{count} Erinnerungen', + 'detail.schedule.daily': 'Täglich um {time} ({zone})', + 'detail.schedule.weekly': 'Wöchentlich auf {days} um {time} ({zone})', + 'detail.schedule.cron': 'Cron {expression} ({zone})', + 'detail.weekday.1': 'Mo', + 'detail.weekday.2': 'Di', + 'detail.weekday.3': 'Mi', + 'detail.weekday.4': 'Do', + 'detail.weekday.5': 'Fr', + 'detail.weekday.6': 'Sa', + 'detail.weekday.7': 'Sonne', + 'detail.weekday.join': ', ', + 'tool.title.inspectProviders': 'Anbieter prüfen', + 'tool.title.queryRuntime': 'Abfragelaufzeit', + 'tool.title.inspectPlugins': 'Plugins prüfen', + 'tool.title.workflow': 'Workflow ausführen', + 'tool.title.ralph': 'Führen Sie die Ralph-Schleife aus', + 'tool.title.readEvent': 'Veranstaltung lesen', + 'tool.title.searchEvents': 'Suchen Sie nach Ereignissen', + 'tool.title.traceEvent': 'Trace-Ereignis', + 'tool.title.searchSessions': 'Sitzungen suchen', + 'tool.title.traceSession': 'Trace-Sitzung', + 'tool.title.listModels': 'Modelle auflisten', + 'tool.title.subagent': 'Subagent erstellen', + 'tool.title.listAgents': 'Subagenten auflisten', + 'tool.title.sendMessage': 'Nachricht senden', + 'tool.title.interruptAgent': 'Interrupt-Agent', + 'tool.title.listJobs': 'Hintergrundjobs auflisten', + 'tool.title.readJob': 'Jobausgabe lesen', + 'tool.title.killJob': 'Hintergrundjob abbrechen', + 'tool.title.openTerminal': 'Terminal öffnen', + 'tool.title.readTerminal': 'Terminal lesen', + 'tool.title.listTerminals': 'Terminals auflisten', + 'tool.title.signalTerminal': 'Signalklemme', + 'tool.title.closeTerminal': 'Terminal schließen', + 'tool.title.lsp': 'Codesymbole abfragen', + 'tool.title.findDefinition': 'Definition finden', + 'tool.title.findReferences': 'Finden Sie Referenzen', + 'tool.title.findImplementation': 'Umsetzung finden', + 'tool.title.hoverSymbol': 'Symbol prüfen', + 'tool.title.spawnTeammate': 'Teamkollege erstellen', + 'tool.title.createTeamTask': 'Teamaufgabe erstellen', + 'tool.title.getTeamTask': 'Teamaufgabe lesen', + 'tool.title.updateTeamTask': 'Teamaufgabe aktualisieren', + 'tool.title.listTeamTasks': 'Teamaufgaben auflisten', + 'tool.title.waitAgent': 'Warten Sie auf den Subagenten', + 'detail.recordedResult': 'Aufgezeichnetes Ergebnis', + 'detail.empty': 'Keine Ergebnisse', + 'detail.none': 'Keine', + 'detail.yes': 'Ja', + 'detail.no': 'Nein', + 'detail.moreInInspect': '{count} weitere Artikel in Inspect verfügbar', + 'detail.status.running': 'Laufen', + 'detail.status.idle': 'Leerlauf', + 'detail.status.ready': 'Bereit', + 'detail.status.inactive': 'Inaktiv', + 'detail.status.provisioning': 'Bereitstellung', + 'detail.status.failed': 'Fehlgeschlagen', + 'detail.status.completed': 'Abgeschlossen', + 'detail.status.deleted': 'Gelöscht', + 'detail.status.killed': 'Abgesagt', + 'detail.status.accepted': 'Akzeptiert', + 'detail.status.queued': 'In der Warteschlange', + 'detail.status.exited': 'Ausgegangen', + 'detail.field.id': 'ID', + 'detail.field.revision': 'Überarbeitung', + 'detail.field.platform': 'Plattform', + 'detail.field.provider': 'Anbieter', + 'detail.field.model': 'Modell', + 'detail.field.role': 'Rolle', + 'detail.field.context': 'Kontext', + 'detail.field.owner': 'Besitzer', + 'detail.field.ready': 'Bereit', + 'detail.field.dependencies': 'Abhängigkeiten', + 'detail.field.writeScopes': 'Bereiche schreiben', + 'detail.field.warnings': 'Warnungen', + 'detail.field.diagnostics': 'Diagnose', + 'detail.field.methods': 'Methoden', + 'detail.field.inputSchema': 'Eingabeschema', + 'detail.field.outputSchema': 'Ausgabeschema', + 'detail.field.currentPackage': 'Aktuelles Paket', + 'detail.field.nextPackage': 'Nächstes Paket', + 'detail.field.latestRun': 'Letzter Lauf', + 'detail.field.packages': 'Pakete', + 'detail.field.registrations': 'Anmeldungen', + 'detail.field.props': 'Requisiten', + 'detail.field.data': 'Daten', + 'detail.field.source': 'Quelle', + 'detail.field.content': 'Inhalt', + 'detail.field.message': 'Nachricht', + 'detail.field.messageId': 'Nachrichten-ID', + 'detail.field.root': 'Wurzel', + 'detail.field.pid': 'Prozess-ID', + 'detail.field.type': 'Typ', + 'detail.field.time': 'Zeit', + 'detail.field.seq': 'Ereignissequenz', + 'detail.field.turn': 'Runde', + 'detail.field.step': 'Schritt', + 'detail.field.callId': 'Aufruf-ID', + 'detail.field.agents': 'Agenten begannen', + 'detail.field.result': 'Ergebnis', + 'detail.field.parent': 'Eltern', + 'detail.field.depth': 'Tiefe', + 'detail.field.exitCode': 'Exit-Code', + 'detail.field.signal': 'Signal', + 'detail.field.previousStatus': 'Vorheriger Stand', + 'detail.field.agent': 'Agenten-ID', + 'detail.field.job': 'Job-ID', + 'detail.field.task': 'Aufgabe', + 'detail.field.processGroup': 'Prozessgruppe', + 'detail.field.availability': 'Verfügbarkeit', + 'detail.field.bestMatch': 'Beste Übereinstimmung', + 'detail.field.target': 'Zielereignis', + 'detail.field.surface': 'Aufnahmestatus', + 'detail.agents.count': '{count}-Agenten', + 'detail.jobs.count': '{count} Hintergrundjobs', + 'detail.terminals.count': '{count}-Klemmen', + 'detail.tasks.count': '{count} Teamaufgaben', + 'detail.tasks.nextPage': 'Weitere Aufgaben verfügbar; Der nächste Cursor ist {cursor}', + 'detail.locations.count': '{count}-Standorte', + 'detail.location': 'Zeile {line}, Spalte {column}', + 'detail.receipt.delivered': 'Nachricht zugestellt', + 'detail.receipt.interrupt': 'Unterbrechung angefordert', + 'detail.receipt.started': 'Begonnen', + 'detail.receipt.cancel': 'Stornierung beantragt', + 'detail.receipt.alreadyFinished': 'Schon fertig', + 'detail.receipt.signal': 'Signal geliefert', + 'detail.receipt.closed': 'Geschlossen', + 'detail.receipt.closing': 'Schließung', + 'detail.wait.noProgress': 'Keine aktiven Subagenten', + 'detail.wait.title': 'Aktivität des Subagenten', + 'detail.wait.timeout': 'Wartezeit abgelaufen', + 'detail.wait.changed': 'Änderung erkannt', + 'detail.agent.reply': 'Antwort des Agenten', + 'detail.models.title': 'Verfügbare Modelle', + 'detail.output.lines': 'Linien {begin}–{end} von {total}', + 'detail.output.truncated': 'Ausgabe abgeschnitten', + 'detail.providers.count': '{count} prüft Anbieter', + 'detail.plugins.count': 'Dynamische {count}-Plugins', + 'detail.workflow.script': 'Workflow-Skript', + 'detail.ralph.reportedComplete': 'Der Arbeiter hat die Fertigstellung gemeldet', + 'detail.ralph.reportedBlocker': 'Der Arbeiter hat einen Blocker gemeldet', + 'detail.ralph.limit': 'Rundenlimit erreicht', + 'detail.report.nextSteps': 'Verbleibende Arbeit', + 'detail.trace.replacedBy': 'Ersetzt durch', + 'detail.trace.replacementChain': 'Ersatzkette', + 'detail.trace.replaces': 'Ereignisse ersetzt', + 'detail.trace.sources': 'Quellereignisse', + 'detail.trace.derived': 'Abgeleitete Ereignisse', + 'detail.trace.ancestors': 'Ahnensitzungen', + 'detail.trace.descendants': 'Nachkommende Sitzungen', + 'detail.matches.count': '{count} Übereinstimmungen', + 'detail.matches.capped': 'Ergebnisgrenze erreicht; Grenzen Sie die Suche auf mehr ein', + 'detail.event.neighbors': 'Umliegende Veranstaltungen', + 'todo.completed': '{done}/{total} abgeschlossen', + 'command.attachmentsUnsupported': '/{command} akzeptiert keine Anhänge; Entfernen Sie sie zuerst', + 'ask.rowTitle': 'Frage stellen', + 'ask.waiting': 'warten', + 'ask.pending': 'Fortsetzung; Antwort noch verfügbar', + 'ask.pendingDetail': 'Diese offenen Fragen bleiben vom Komponisten zu beantworten.', + 'ask.reopen': 'Antwort', + 'ask.review': 'Antworten anzeigen', + 'ask.closed': 'geschlossen', + 'ask.closedDetail': 'Diese Frage ist geschlossen; Das Ergebnis finden Sie im folgenden Gespräch.', + 'ask.cancelled': 'abgesagt', + 'ask.cancelledDetail': 'Dieser Fragensatz wurde gelöscht, bevor die Antworten übermittelt wurden.', + 'ask.interrupted': 'unterbrochen', + 'ask.interruptedDetail': 'Dieser Fragensatz wurde unterbrochen, bevor Antworten übermittelt wurden.', + 'ask.answered': '{answered}/{total} antwortete', + 'ask.skipped': 'Nicht beantwortet', + 'bash.running': 'Laufen', + 'bash.failed': 'Fehlgeschlagen', + 'bash.stopped': 'Angehalten', + 'row.running': 'Laufen', + 'row.preparing': 'Werkzeugaufruf wird vorbereitet', + 'row.failed': 'Fehlgeschlagen', + 'row.stopped': 'Angehalten', + 'row.commandArguments': 'Befehlsargumente', + 'row.input': 'IN', + 'row.output': 'OUT', + 'row.inspect': 'Inspizieren', + 'row.imageSize': '{width}x{height}px, {bytes} Bytes', + 'row.imageSizeDownscaled': '{width}x{height}px (herunterskaliert von {originalWidth}x{originalHeight}px), {bytes} Bytes', + 'tool.title.search': 'Suchen', + 'tool.title.read': 'Lesen', + 'tool.title.bash': 'Bash', + 'tool.title.write': 'Schreiben', + 'tool.title.edit': 'Bearbeiten', + 'tool.title.code': 'Code', + 'tool.title.generic': 'Werkzeugaufruf', + 'tool.title.inspect': 'Fragen Sie die Cordis-Umgebung ab', + 'tool.title.runCordis': 'Führen Sie das Cordis-Plugin aus', + 'tool.title.stopCordis': 'Stoppen Sie das Cordis-Plugin', + 'tool.title.removeCordis': 'Entfernen Sie das Cordis-Plugin', + 'tool.title.pwsh': 'Pwsh', + 'tool.title.readImage': 'Bild lesen', + 'tool.title.grep': 'Grep', + 'tool.title.glob': 'Globus', + 'tool.title.webSearch': 'Suchen', + 'tool.title.webFetch': 'Holen', + 'tool.autoReviewRejected': 'Abgelehnt von Auto review', + 'tool.autoReviewNotExecuted': 'Werkzeug wurde nicht ausgeführt. Grund: {reason}', + 'tool.autoReviewReasonFallback': 'Auto review hat diese Aktion nicht autorisiert', + 'diff.collapseAria': 'Differenz ausblenden', + 'diff.expandAria': 'Erweitern Sie {count} um weitere Diff-Linien', + 'diff.expandRest': '… {count} weitere Zeilen', + 'read.window': 'Zeigt {shown} der {total}-Linien', + 'read.collapseAria': 'Inhalt reduzieren', + 'read.expandAria': 'Erweitern Sie {count} um weitere Zeilen', + 'read.expandRest': '… {count} weitere Zeilen', + 'search.paths': '{shown}-Pfade', + 'search.paths.truncated': 'Zeigt {shown} von {total}-Pfaden', + 'search.matches': '{shown} entspricht · {files}-Dateien', + 'search.matches.truncated': 'Zeigt {shown} von {total}-Übereinstimmungen · {files}-Dateien', + 'search.noResults': 'Keine Ergebnisse', + 'search.collapseAria': 'Ergebnisse reduzieren', + 'search.expandAria': 'Erweitern Sie {count} um weitere Ergebniszeilen', + 'search.expandRest': '… {count} weitere Zeilen', + 'web.noResults': 'Keine Ergebnisse gefunden', + 'web.sourcesTruncated': 'Quellenliste gekürzt', + 'web.http': 'HTTP', + 'web.contentTruncated': 'Inhalt abgeschnitten', + 'details.running': 'Laufen…', + 'queue.count': '{n} Nachrichten in der Warteschlange', + 'queue.sending': 'Senden…', + 'queue.image': 'Nachrichtenbild in der Warteschlange', + 'queue.file': 'Datei in der Warteschlange {name}', + 'queue.edit': 'Bearbeiten Sie die Nachricht in der Warteschlange', + 'queue.edit.unsupported': 'Enthält Nicht-Textinhalte; Bearbeitung wird noch nicht unterstützt', + 'queue.save': 'Speichern Sie die Nachricht in der Warteschlange', + 'queue.cancelEdit': 'Bearbeitung abbrechen', + 'queue.remove': 'Entfernen Sie die Nachricht in der Warteschlange', + 'queue.steer': 'Mit vorgemerkter Nachricht lenken', + 'queue.steer.unavailable': 'Die Steuerung ist nur verfügbar, während der Agent ausgeführt wird', + 'error.sessionInUse': 'Diese Sitzung wird bereits verwendet, möglicherweise von einer anderen laufenden DSH-Instanz (z. B. dsh Web oder der Desktop-App). Beenden Sie andere laufende DSH-Instanzen und versuchen Sie es erneut.', + 'queue.editFailed': 'Bearbeitung fehlgeschlagen: Der Versand dieser Nachricht wurde möglicherweise bereits gestartet.', + 'queue.removeFailed': 'Fehler beim Entfernen: Der Versand dieser Nachricht wurde möglicherweise bereits gestartet.', + 'queue.steerFailed': 'Lenkung versagt. Versuchen Sie es erneut.', + 'terminal.signal': 'Signal {signal}', + 'terminal.exitCode': 'Exit-Code {code}', + 'terminal.noExitCode': 'kein Exit-Code', + 'terminal.commandLine': 'Befehlszeile {n}', + 'terminal.running': 'Laufen', + 'terminal.failed': 'Fehlgeschlagen', + 'terminal.done': 'Fertig', + 'terminal.noOutput': 'Keine Ausgabe', + 'terminal.collapseAria': 'Ausgabe reduzieren', + 'terminal.expandAria': 'Erweitern Sie die verbleibenden {n}-Ausgabezeilen', + 'terminal.expandRest': '… {n} weitere Zeilen', + // The Host terminal_send presenter has no locale seat; keep its fallbacks + // aligned with these English values. + 'terminal.sendInput': '(Eingabe senden)', + 'terminal.session': 'Terminal {sessionId}', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'quote.remove': 'Удалить цитату', + 'shortcut.newline': 'Новая линия', + 'shortcut.complementary': 'Использовать альтернативное действие: поставить в очередь или направить', + 'shortcut.slash': 'Открыть командное меню', + 'shortcut.mention': 'Открыть справочное меню', + 'hint.plan': PLAN_NEXT_ACTION_EN, + 'hint.goal': 'описать цель долгосрочной задачи', + 'hint.goal.active': 'цель активна — изменить/приостановить/возобновить/очистить', + 'placeholder.plan': PLAN_NEXT_ACTION_EN, + 'placeholder.default': 'Отправьте сообщение или запустите задачу, /команды, @файлы или сеансы', + 'placeholder.unavailable': 'Сеанс недоступен', + 'placeholder.parentOffline': 'Родительская сессия в автономном режиме; отправка недоступна, но вы все равно можете остановить запуск', + 'placeholder.hero': 'Опишите, что вы хотите построить: /команды, @файлы или сеансы.', + 'placeholder.workspace': 'Выберите рабочее пространство для начала', + 'placeholder.steerQueue': 'Cmd/Ctrl+Enter управляет всеми сообщениями в очереди.', + 'input.commands': 'Добавляйте файлы или запускайте команды', + 'input.file': 'Файл', + 'input.stop': 'Остановить генерацию', + 'input.send': 'Отправить сообщение', + 'input.send.queue': 'Сообщение в очереди', + 'input.send.steer': 'Направить сообщение', + 'attachment.pending': 'Ожидающие вложения', + 'attachment.scrollLeft': 'Прокрутить вложения влево', + 'attachment.scrollRight': 'Прокрутить вложения вправо', + 'attachment.dropTitle': 'Перетащите файлы или изображения сюда, чтобы добавить их.', + 'attachment.dropDesc': 'Ограничение изображений: до изображений {count}, по {size} каждое.', + 'attachment.dropBlocked': 'Файлы и изображения не могут быть добавлены прямо сейчас.', + 'attachment.directoryDesktopOnly': 'Папки можно добавлять только в настольном приложении; добавлять отдельные файлы в браузере', + 'attachment.pathUnavailable': 'Не удалось получить путь к папке; перетащите это снова', + 'attachment.pathUnsupported': 'Путь содержит символы, которые не может содержать ссылка; переименуй его и попробуй еще раз', + 'image.pending': 'Ожидающие изображения', + 'image.openOriginal': 'Посмотреть оригинал', + 'image.openOriginalLabel': '{label}, нажмите, чтобы посмотреть оригинал', + 'image.remove': 'Удалить изображение {name}', + 'image.original': 'Исходное изображение', + 'image.label': 'Изображение', + 'image.loadFailed': 'Изображение не удалось загрузить; нажмите, чтобы повторить попытку', + 'image.loading': 'Загрузка изображения…', + 'image.preview': 'Предварительный просмотр исходного изображения', + 'image.closePreview': 'Закрыть предварительный просмотр исходного изображения', + 'image.unsupportedType': 'Поддерживаются только изображения PNG, JPG, WebP и GIF.', + 'image.tooMany': 'Сообщение может содержать до {count} изображений.', + 'image.fileTooLarge': 'Каждое изображение должно быть меньше {size}.', + 'image.totalTooLarge': 'Общее количество изображений превышает {size}; удалите некоторые и попробуйте еще раз', + 'image.tooManyPixels': 'Разрешение изображения слишком высокое; сожми его и попробуй еще раз', + 'image.dimensionTooLarge': 'Стороны изображения должны быть не более {size}px; уменьшите масштаб и попробуйте еще раз', + 'image.modelUnsupported': 'Текущая модель не поддерживает изображения; переключиться на модель, которая делает', + 'image.sendFailed': 'Отправка изображений не удалась ({reason}); добавьте их повторно и попробуйте еще раз', + 'file.pending': 'Ожидающие файлы', + 'file.remove': 'Удалить файл {name}', + 'file.uploading': 'Загрузка…', + 'file.uploadFailed': 'Загрузка не удалась; нажмите, чтобы повторить попытку', + 'file.retry': 'Повторите загрузку {name}.', + 'file.stillUploading': 'Файлы все еще загружаются; отправить после того, как они закончат', + 'file.sessionUnavailable': 'Сессия недоступна; файлы не могут быть загружены', + 'file.notStaged': 'Загрузка файла не завершена; добавьте его еще раз и попробуйте еще раз', + 'file.label': 'Файл', + 'context.aria': '{percent} используемого контекста', + 'context.used': 'используемого контекста', + 'context.system': 'Системная подсказка', + 'context.tools': 'Определения инструментов', + 'context.messages': 'Сообщения', + 'settings.enter.title': 'Отправлять поведение во время занятости', + 'settings.enter.description': 'Что делают кнопки «Ввод» и «Отправить» во время работы агента; Cmd/Ctrl+Enter использует другое поведение.', + 'settings.enter.queue': 'Очередь', + 'settings.enter.steer': 'Направить', + 'hero.headline': 'В неизвестность', + 'hero.preview': 'Предварительный просмотр', + 'hero.chooseWorkspace': 'Выберите рабочее пространство', + 'session.hierarchy': 'Иерархия сеансов', + 'session.moreViews': 'Больше просмотров беседы', + 'todo.title': 'Дела', + 'todo.progress.done': '{done} завершено', + 'todo.progress.active': '{active} в процессе', + 'todo.progress.pending': '{pending} в ожидании', + 'todo.status.completed': 'Завершено', + 'todo.status.inProgress': 'В процессе', + 'todo.status.pending': 'Ожидается', + 'todo.rowTitle': 'Обновить список дел', + 'tool.title.createGoal': 'Создать цель', + 'tool.title.getGoal': 'Посмотреть цель', + 'tool.title.updateGoal': 'Обновить цель', + 'tool.preparing.content': '{kilobytes}KB', + 'tool.title.createSchedule': 'Создать напоминание', + 'tool.title.listSchedules': 'Список напоминаний', + 'tool.title.deleteSchedule': 'Удалить напоминание', + 'tool.title.updateSchedule': 'Обновить напоминание', + 'detail.state': 'Статус', + 'detail.todo.completed': 'Завершено', + 'detail.todo.in_progress': 'В процессе', + 'detail.todo.pending': 'Ожидается', + 'detail.todo.empty': 'Список дел пуст', + 'todo.diff.initial': 'Первоначальный список', + 'todo.diff.compare': 'Изменения по сравнению с предыдущим списком', + 'todo.diff.unavailable': 'Предыдущий список недоступен', + 'todo.diff.noChanges': 'Никаких изменений в списке', + 'todo.diff.added': '{count} добавлен', + 'todo.diff.updated': '{count} обновлен', + 'todo.diff.removed': '{count} удален.', + 'todo.diff.unchanged': '{count} без изменений', + 'todo.diff.addedItem': 'Добавлено', + 'todo.diff.updatedItem': 'Статус изменен', + 'todo.diff.movedItem': 'Переупорядочен', + 'todo.diff.removedItem': 'Удален', + 'detail.goal.empty': 'Нет цели', + 'detail.goal.active': 'Активный', + 'detail.goal.disarmed': 'Ожидание продолжения', + 'detail.goal.paused': 'Приостановлено', + 'detail.goal.blocked': 'Заблокировано', + 'detail.goal.complete': 'Завершено', + 'detail.goal.rounds': 'Раунды', + 'detail.goal.reason': 'Блокатор', + 'detail.days': '{count} д', + 'detail.hours': '{count} ч', + 'detail.minutes': '{count} мин.', + 'detail.seconds': '{count} с', + 'detail.schedule.once': 'Однажды', + 'detail.schedule.every': 'Каждый {interval}', + 'detail.schedule.when': 'Запланировано на', + 'detail.schedule.frequency': 'Повторить', + 'detail.schedule.scheduled': 'Запланировано', + 'detail.schedule.overdue': 'Просрочено, ждём возобновления сеанса', + 'detail.schedule.empty': 'Нет напоминаний', + 'detail.schedule.deleted': 'Удалено', + 'detail.schedule.count': '{count} напоминания', + 'detail.schedule.daily': 'Каждый день в {time} ({zone})', + 'detail.schedule.weekly': 'Еженедельно на {days} по адресу {time} ({zone})', + 'detail.schedule.cron': 'Крон {expression} ({zone})', + 'detail.weekday.1': 'Пн.', + 'detail.weekday.2': 'Вт', + 'detail.weekday.3': 'ср.', + 'detail.weekday.4': 'Чт', + 'detail.weekday.5': 'Пт', + 'detail.weekday.6': 'Суббота', + 'detail.weekday.7': 'Солнце', + 'detail.weekday.join': ', ', + 'tool.title.inspectProviders': 'Проверка поставщиков', + 'tool.title.queryRuntime': 'Время выполнения запроса', + 'tool.title.inspectPlugins': 'Проверьте плагины', + 'tool.title.workflow': 'Запустить рабочий процесс', + 'tool.title.ralph': 'Запустить цикл Ральфа', + 'tool.title.readEvent': 'Чтение события', + 'tool.title.searchEvents': 'Поиск событий', + 'tool.title.traceEvent': 'Отследить событие', + 'tool.title.searchSessions': 'Поиск сеансов', + 'tool.title.traceSession': 'Трассировка сеанса', + 'tool.title.listModels': 'Список моделей', + 'tool.title.subagent': 'Создать субагент', + 'tool.title.listAgents': 'Список субагентов', + 'tool.title.sendMessage': 'Отправить сообщение', + 'tool.title.interruptAgent': 'Агент прерываний', + 'tool.title.listJobs': 'Список фоновых заданий', + 'tool.title.readJob': 'Чтение результатов задания', + 'tool.title.killJob': 'Отменить фоновое задание', + 'tool.title.openTerminal': 'Открыть терминал', + 'tool.title.readTerminal': 'Чтение терминала', + 'tool.title.listTerminals': 'Список терминалов', + 'tool.title.signalTerminal': 'Сигнальный терминал', + 'tool.title.closeTerminal': 'Закрыть терминал', + 'tool.title.lsp': 'Символы кода запроса', + 'tool.title.findDefinition': 'Найти определение', + 'tool.title.findReferences': 'Найдите ссылки', + 'tool.title.findImplementation': 'Найти реализацию', + 'tool.title.hoverSymbol': 'Символ проверки', + 'tool.title.spawnTeammate': 'Создать товарища по команде', + 'tool.title.createTeamTask': 'Создать командную задачу', + 'tool.title.getTeamTask': 'Прочитать командное задание', + 'tool.title.updateTeamTask': 'Обновить групповую задачу', + 'tool.title.listTeamTasks': 'Перечислить командные задачи', + 'tool.title.waitAgent': 'Подождите субагента', + 'detail.recordedResult': 'Записанный результат', + 'detail.empty': 'Нет результатов', + 'detail.none': 'Нет', + 'detail.yes': 'Да', + 'detail.no': 'Нет', + 'detail.moreInInspect': '{count} больше элементов доступно в Inspect', + 'detail.status.running': 'Бег', + 'detail.status.idle': 'Простой', + 'detail.status.ready': 'Готово', + 'detail.status.inactive': 'Неактивный', + 'detail.status.provisioning': 'обеспечение', + 'detail.status.failed': 'Не удалось', + 'detail.status.completed': 'Завершено', + 'detail.status.deleted': 'Удалено', + 'detail.status.killed': 'Отменено', + 'detail.status.accepted': 'Принято', + 'detail.status.queued': 'В очереди', + 'detail.status.exited': 'Вышел', + 'detail.field.id': 'ID', + 'detail.field.revision': 'Редакция', + 'detail.field.platform': 'Платформа', + 'detail.field.provider': 'Поставщик', + 'detail.field.model': 'Модель', + 'detail.field.role': 'Роль', + 'detail.field.context': 'Контекст', + 'detail.field.owner': 'Владелец', + 'detail.field.ready': 'Готово', + 'detail.field.dependencies': 'Зависимости', + 'detail.field.writeScopes': 'Запись областей', + 'detail.field.warnings': 'Предупреждения', + 'detail.field.diagnostics': 'Диагностика', + 'detail.field.methods': 'Методы', + 'detail.field.inputSchema': 'Входная схема', + 'detail.field.outputSchema': 'Выходная схема', + 'detail.field.currentPackage': 'Текущий пакет', + 'detail.field.nextPackage': 'Следующий пакет', + 'detail.field.latestRun': 'Последний запуск', + 'detail.field.packages': 'Пакеты', + 'detail.field.registrations': 'Регистрации', + 'detail.field.props': 'Реквизит', + 'detail.field.data': 'Данные', + 'detail.field.source': 'Источник', + 'detail.field.content': 'Содержание', + 'detail.field.message': 'Сообщение', + 'detail.field.messageId': 'Идентификатор сообщения', + 'detail.field.root': 'Корень', + 'detail.field.pid': 'Идентификатор процесса', + 'detail.field.type': 'Тип', + 'detail.field.time': 'Время', + 'detail.field.seq': 'Последовательность событий', + 'detail.field.turn': 'Ход', + 'detail.field.step': 'Шаг', + 'detail.field.callId': 'Идентификатор вызова', + 'detail.field.agents': 'Агенты запущены', + 'detail.field.result': 'Результат', + 'detail.field.parent': 'Родитель', + 'detail.field.depth': 'Глубина', + 'detail.field.exitCode': 'Код выхода', + 'detail.field.signal': 'Сигнал', + 'detail.field.previousStatus': 'Предыдущий статус', + 'detail.field.agent': 'Идентификатор агента', + 'detail.field.job': 'Идентификатор вакансии', + 'detail.field.task': 'Задача', + 'detail.field.processGroup': 'Группа процессов', + 'detail.field.availability': 'Доступность', + 'detail.field.bestMatch': 'Лучший матч', + 'detail.field.target': 'Целевое событие', + 'detail.field.surface': 'Статус записи', + 'detail.agents.count': '{count} агенты', + 'detail.jobs.count': '{count} фоновые задания', + 'detail.terminals.count': 'Клеммы {count}', + 'detail.tasks.count': '{count} командные задачи', + 'detail.tasks.nextPage': 'Доступно больше задач; следующий курсор — {cursor}', + 'detail.locations.count': '{count} локации', + 'detail.location': 'Строка {line}, столбец {column}', + 'detail.receipt.delivered': 'Сообщение доставлено', + 'detail.receipt.interrupt': 'Запрошено прерывание', + 'detail.receipt.started': 'Началось', + 'detail.receipt.cancel': 'Запрос на отмену', + 'detail.receipt.alreadyFinished': 'Уже закончено', + 'detail.receipt.signal': 'Сигнал доставлен', + 'detail.receipt.closed': 'Закрыто', + 'detail.receipt.closing': 'Закрытие', + 'detail.wait.noProgress': 'Нет активных субагентов', + 'detail.wait.title': 'Субагентская деятельность', + 'detail.wait.timeout': 'Время ожидания истекло', + 'detail.wait.changed': 'Обнаружено изменение', + 'detail.agent.reply': 'Ответ агента', + 'detail.models.title': 'Доступные модели', + 'detail.output.lines': 'Линии {begin} – {end} из {total}', + 'detail.output.truncated': 'Вывод усечен', + 'detail.providers.count': '{count} проверяет поставщиков', + 'detail.plugins.count': 'Динамические плагины {count}', + 'detail.workflow.script': 'Сценарий рабочего процесса', + 'detail.ralph.reportedComplete': 'Работник сообщил о завершении', + 'detail.ralph.reportedBlocker': 'Работник сообщил о блокировщике', + 'detail.ralph.limit': 'Достигнут лимит раундов', + 'detail.report.nextSteps': 'Оставшаяся работа', + 'detail.trace.replacedBy': 'Заменено на', + 'detail.trace.replacementChain': 'Замена цепи', + 'detail.trace.replaces': 'Замененные события', + 'detail.trace.sources': 'Исходные события', + 'detail.trace.derived': 'Производные события', + 'detail.trace.ancestors': 'Сеансы предков', + 'detail.trace.descendants': 'Потомки сеансов', + 'detail.matches.count': '{count} совпадений', + 'detail.matches.capped': 'Достигнут предел результатов; сузить поиск для большего', + 'detail.event.neighbors': 'Сопутствующие события', + 'todo.completed': '{done}/{total} завершено', + 'command.attachmentsUnsupported': '/{command} не принимает вложения; сначала удали их', + 'ask.rowTitle': 'Задать вопрос', + 'ask.waiting': 'ожидание', + 'ask.pending': 'продолжение; ответ еще доступен', + 'ask.pendingDetail': 'На эти нерешенные вопросы остается ответ композитора.', + 'ask.reopen': 'Ответ', + 'ask.review': 'Посмотреть ответы', + 'ask.closed': 'закрыто', + 'ask.closedDetail': 'Этот вопрос закрыт; его результат в разговоре ниже.', + 'ask.cancelled': 'отменен', + 'ask.cancelledDetail': 'Этот набор вопросов был отменен до того, как были отправлены ответы.', + 'ask.interrupted': 'прерванный', + 'ask.interruptedDetail': 'Этот набор вопросов был прерван до того, как были представлены ответы.', + 'ask.answered': '{answered}/{total} ответил', + 'ask.skipped': 'Не ответил', + 'bash.running': 'Бег', + 'bash.failed': 'Не удалось', + 'bash.stopped': 'Остановлено', + 'row.running': 'Бег', + 'row.preparing': 'Подготовка вызова инструмента', + 'row.failed': 'Не удалось', + 'row.stopped': 'Остановлено', + 'row.commandArguments': 'Аргументы команды', + 'row.input': 'IN', + 'row.output': 'OUT', + 'row.inspect': 'Осмотреть', + 'row.imageSize': '{width}x{height}px, {bytes} байт', + 'row.imageSizeDownscaled': '{width}x{height}px (уменьшено с {originalWidth}x{originalHeight}px), {bytes} байт', + 'tool.title.search': 'Поиск', + 'tool.title.read': 'Читать', + 'tool.title.bash': 'Баш', + 'tool.title.write': 'Написать', + 'tool.title.edit': 'Редактировать', + 'tool.title.code': 'Код', + 'tool.title.generic': 'Вызов инструмента', + 'tool.title.inspect': 'Запрос среды Cordis', + 'tool.title.runCordis': 'Запустите плагин Cordis', + 'tool.title.stopCordis': 'Остановить плагин Cordis', + 'tool.title.removeCordis': 'Удалить плагин Cordis', + 'tool.title.pwsh': 'Пшш', + 'tool.title.readImage': 'Читать изображение', + 'tool.title.grep': 'Греп', + 'tool.title.glob': 'Глоб', + 'tool.title.webSearch': 'Поиск', + 'tool.title.webFetch': 'Получить', + 'tool.autoReviewRejected': 'Отклонено Auto review', + 'tool.autoReviewNotExecuted': 'Инструмент не был выполнен. Причина: {reason}', + 'tool.autoReviewReasonFallback': 'Auto review не санкционировал это действие', + 'diff.collapseAria': 'Свернуть разницу', + 'diff.expandAria': 'Разверните {count} больше строк различий', + 'diff.expandRest': '… {count} еще строки', + 'read.window': 'Показаны линии {shown} {total}.', + 'read.collapseAria': 'Свернуть содержимое', + 'read.expandAria': 'Разверните {count} еще строки', + 'read.expandRest': '… {count} еще строки', + 'search.paths': '{shown} пути', + 'search.paths.truncated': 'Отображение путей {shown} из {total}', + 'search.matches': '{shown} соответствует · файлам {files}', + 'search.matches.truncated': 'Отображение {shown} совпадений {total} · Файлы {files}', + 'search.noResults': 'Нет результатов', + 'search.collapseAria': 'Свернуть результаты', + 'search.expandAria': 'Разверните {count} больше строк результатов', + 'search.expandRest': '… {count} еще строки', + 'web.noResults': 'Результаты не найдены', + 'web.sourcesTruncated': 'Список источников сокращен', + 'web.http': 'HTTP', + 'web.contentTruncated': 'Содержание усечено', + 'details.running': 'Бег…', + 'queue.count': 'Сообщения в очереди {n}', + 'queue.sending': 'Отправка…', + 'queue.image': 'Изображение сообщения в очереди', + 'queue.file': 'Файл в очереди {name}', + 'queue.edit': 'Изменить сообщение в очереди', + 'queue.edit.unsupported': 'Содержит нетекстовый контент; редактирование пока не поддерживается', + 'queue.save': 'Сохранить сообщение в очереди', + 'queue.cancelEdit': 'Отменить редактирование', + 'queue.remove': 'Удалить сообщение из очереди', + 'queue.steer': 'Направить сообщение из очереди', + 'queue.steer.unavailable': 'Управление доступно только во время работы агента.', + 'error.sessionInUse': 'Этот сеанс уже используется, возможно, другим работающим экземпляром DSH (например, веб-сайтом dsh или настольным приложением). Закройте другие работающие экземпляры DSH и повторите попытку.', + 'queue.editFailed': 'Не удалось изменить: возможно, это сообщение уже начало отправляться.', + 'queue.removeFailed': 'Удаление не удалось: возможно, это сообщение уже начало отправляться.', + 'queue.steerFailed': 'Рулевое управление не удалось. Попробуйте еще раз.', + 'terminal.signal': 'сигнал {signal}', + 'terminal.exitCode': 'код выхода {code}', + 'terminal.noExitCode': 'нет кода выхода', + 'terminal.commandLine': 'Командная строка {n}', + 'terminal.running': 'Бег', + 'terminal.failed': 'Не удалось', + 'terminal.done': 'Готово', + 'terminal.noOutput': 'Нет вывода', + 'terminal.collapseAria': 'Свернуть вывод', + 'terminal.expandAria': 'Разверните оставшиеся выходные строки {n}.', + 'terminal.expandRest': '… {n} еще строки', + // The Host terminal_send presenter has no locale seat; keep its fallbacks + // aligned with these English values. + 'terminal.sendInput': '(отправить ввод)', + 'terminal.session': 'Терминал {sessionId}', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'quote.remove': 'Remover cotação', + 'shortcut.newline': 'Nova linha', + 'shortcut.complementary': 'Usar a ação alternativa de enfileirar ou orientar', + 'shortcut.slash': 'Abrir menu de comando', + 'shortcut.mention': 'Abrir menu de referência', + 'hint.plan': PLAN_NEXT_ACTION_EN, + 'hint.goal': 'descrever o objetivo de uma tarefa de longa duração', + 'hint.goal.active': 'meta ativa — editar/pausar/resumir/limpar', + 'placeholder.plan': PLAN_NEXT_ACTION_EN, + 'placeholder.default': 'Envie mensagens ou execute uma tarefa, / comandos, @ arquivos ou sessões', + 'placeholder.unavailable': 'Sessão indisponível', + 'placeholder.parentOffline': 'Sessão pai offline; o envio não está disponível, mas você ainda pode interromper a execução', + 'placeholder.hero': 'Descreva o que você deseja construir, / comandos, @ arquivos ou sessões', + 'placeholder.workspace': 'Escolha um espaço de trabalho para começar', + 'placeholder.steerQueue': 'Cmd/Ctrl+Enter direciona todas as mensagens na fila', + 'input.commands': 'Adicione arquivos ou execute comandos', + 'input.file': 'Arquivo', + 'input.stop': 'Parar geração', + 'input.send': 'Enviar mensagem', + 'input.send.queue': 'Mensagem de fila', + 'input.send.steer': 'Orientar com mensagem', + 'attachment.pending': 'Anexos pendentes', + 'attachment.scrollLeft': 'Rolar anexos para a esquerda', + 'attachment.scrollRight': 'Rolar os anexos para a direita', + 'attachment.dropTitle': 'Arraste arquivos ou imagens aqui para adicioná-los', + 'attachment.dropDesc': 'Limite de imagens: até {count} imagens, {size} cada', + 'attachment.dropBlocked': 'Arquivos e imagens não podem ser adicionados no momento', + 'attachment.directoryDesktopOnly': 'As pastas só podem ser adicionadas no aplicativo de desktop; adicione arquivos individuais no navegador', + 'attachment.pathUnavailable': 'Não foi possível obter o caminho da pasta; arraste-o novamente', + 'attachment.pathUnsupported': 'O caminho contém caracteres que uma referência não pode transportar; renomeie-o e tente novamente', + 'image.pending': 'Imagens pendentes', + 'image.openOriginal': 'Ver original', + 'image.openOriginalLabel': '{label}, clique para ver o original', + 'image.remove': 'Remover imagem {name}', + 'image.original': 'Imagem original', + 'image.label': 'Imagem', + 'image.loadFailed': 'A imagem não foi carregada; clique para tentar novamente', + 'image.loading': 'Carregando imagem…', + 'image.preview': 'Visualização da imagem original', + 'image.closePreview': 'Fechar visualização da imagem original', + 'image.unsupportedType': 'Somente imagens PNG, JPG, WebP e GIF são suportadas', + 'image.tooMany': 'Uma mensagem pode incluir até {count} imagens', + 'image.fileTooLarge': 'Cada imagem deve ser menor que {size}', + 'image.totalTooLarge': 'As imagens excedem {size} no total; remova alguns e tente novamente', + 'image.tooManyPixels': 'A resolução da imagem é muito alta; comprima e tente novamente', + 'image.dimensionTooLarge': 'Os lados da imagem devem ter no máximo {size}px; diminua a escala e tente novamente', + 'image.modelUnsupported': 'O modelo atual não suporta imagens; mude para um modelo que faça', + 'image.sendFailed': 'Falha no envio de imagens ({reason}); adicione-os novamente e tente novamente', + 'file.pending': 'Arquivos pendentes', + 'file.remove': 'Remover arquivo {name}', + 'file.uploading': 'Fazendo upload…', + 'file.uploadFailed': 'Falha no upload; clique para tentar novamente', + 'file.retry': 'Tente fazer upload novamente {name}', + 'file.stillUploading': 'Os arquivos ainda estão sendo carregados; enviar depois que terminarem', + 'file.sessionUnavailable': 'Sessão indisponível; os arquivos não podem ser carregados', + 'file.notStaged': 'O upload do arquivo não foi concluído; adicione novamente e tente novamente', + 'file.label': 'Arquivo', + 'context.aria': '{percent} de contexto usado', + 'context.used': 'do contexto usado', + 'context.system': 'Alerta do sistema', + 'context.tools': 'Definições de ferramentas', + 'context.messages': 'Mensagens', + 'settings.enter.title': 'Enviar comportamento enquanto estiver ocupado', + 'settings.enter.description': 'O que o botão Enter e o botão Enviar fazem enquanto o agente está em execução; Cmd/Ctrl+Enter usa o outro comportamento', + 'settings.enter.queue': 'Fila', + 'settings.enter.steer': 'Orientar', + 'hero.headline': 'Para o desconhecido', + 'hero.preview': 'Visualização', + 'hero.chooseWorkspace': 'Escolha o espaço de trabalho', + 'session.hierarchy': 'Hierarquia de sessão', + 'session.moreViews': 'Mais visualizações de conversas', + 'todo.title': 'Tarefas', + 'todo.progress.done': '{done} concluído', + 'todo.progress.active': '{active} em andamento', + 'todo.progress.pending': '{pending} pendente', + 'todo.status.completed': 'Concluído', + 'todo.status.inProgress': 'Em andamento', + 'todo.status.pending': 'Pendente', + 'todo.rowTitle': 'Atualizar lista de tarefas', + 'tool.title.createGoal': 'Criar meta', + 'tool.title.getGoal': 'Ver meta', + 'tool.title.updateGoal': 'Atualizar meta', + 'tool.preparing.content': '{kilobytes}KB', + 'tool.title.createSchedule': 'Criar lembrete', + 'tool.title.listSchedules': 'Listar lembretes', + 'tool.title.deleteSchedule': 'Excluir lembrete', + 'tool.title.updateSchedule': 'Lembrete de atualização', + 'detail.state': 'Estado', + 'detail.todo.completed': 'Concluído', + 'detail.todo.in_progress': 'Em andamento', + 'detail.todo.pending': 'Pendente', + 'detail.todo.empty': 'A lista de tarefas está vazia', + 'todo.diff.initial': 'Lista inicial', + 'todo.diff.compare': 'Mudanças desde a lista anterior', + 'todo.diff.unavailable': 'Lista anterior indisponível', + 'todo.diff.noChanges': 'Sem alterações na lista', + 'todo.diff.added': '{count} adicionado', + 'todo.diff.updated': '{count} atualizado', + 'todo.diff.removed': '{count} removido', + 'todo.diff.unchanged': '{count} inalterado', + 'todo.diff.addedItem': 'Adicionado', + 'todo.diff.updatedItem': 'Status alterado', + 'todo.diff.movedItem': 'Reordenado', + 'todo.diff.removedItem': 'Removido', + 'detail.goal.empty': 'Sem objetivo', + 'detail.goal.active': 'Ativo', + 'detail.goal.disarmed': 'Aguardando continuação', + 'detail.goal.paused': 'Pausado', + 'detail.goal.blocked': 'Bloqueado', + 'detail.goal.complete': 'Concluído', + 'detail.goal.rounds': 'Rodadas', + 'detail.goal.reason': 'Bloqueador', + 'detail.days': '{count}d', + 'detail.hours': '{count}h', + 'detail.minutes': '{count} min', + 'detail.seconds': '{count} segundos', + 'detail.schedule.once': 'Uma vez', + 'detail.schedule.every': 'A cada {interval}', + 'detail.schedule.when': 'Agendado para', + 'detail.schedule.frequency': 'Repetir', + 'detail.schedule.scheduled': 'Agendado', + 'detail.schedule.overdue': 'Atrasado, aguardando retomada da sessão', + 'detail.schedule.empty': 'Sem lembretes', + 'detail.schedule.deleted': 'Excluído', + 'detail.schedule.count': '{count} lembretes', + 'detail.schedule.daily': 'Todos os dias às {time} ({zone})', + 'detail.schedule.weekly': 'Semanalmente em {days} às {time} ({zone})', + 'detail.schedule.cron': 'Cron {expression} ({zone})', + 'detail.weekday.1': 'segunda-feira', + 'detail.weekday.2': 'Terça', + 'detail.weekday.3': 'Quarta', + 'detail.weekday.4': 'Qui', + 'detail.weekday.5': 'Sexta-feira', + 'detail.weekday.6': 'Sábado', + 'detail.weekday.7': 'Sol', + 'detail.weekday.join': ', ', + 'tool.title.inspectProviders': 'Inspecionar fornecedores', + 'tool.title.queryRuntime': 'Tempo de execução da consulta', + 'tool.title.inspectPlugins': 'Inspecionar plug-ins', + 'tool.title.workflow': 'Executar fluxo de trabalho', + 'tool.title.ralph': 'Execute o circuito Ralph', + 'tool.title.readEvent': 'Ler evento', + 'tool.title.searchEvents': 'Pesquisar eventos', + 'tool.title.traceEvent': 'Evento de rastreamento', + 'tool.title.searchSessions': 'Buscar sessões', + 'tool.title.traceSession': 'Sessão de rastreamento', + 'tool.title.listModels': 'Listar modelos', + 'tool.title.subagent': 'Criar subagente', + 'tool.title.listAgents': 'Listar subagentes', + 'tool.title.sendMessage': 'Enviar mensagem', + 'tool.title.interruptAgent': 'Agente de interrupção', + 'tool.title.listJobs': 'Listar trabalhos em segundo plano', + 'tool.title.readJob': 'Ler saída do trabalho', + 'tool.title.killJob': 'Cancelar trabalho em segundo plano', + 'tool.title.openTerminal': 'Terminal aberto', + 'tool.title.readTerminal': 'Ler terminal', + 'tool.title.listTerminals': 'Listar terminais', + 'tool.title.signalTerminal': 'Terminal de sinal', + 'tool.title.closeTerminal': 'Fechar terminal', + 'tool.title.lsp': 'Símbolos de código de consulta', + 'tool.title.findDefinition': 'Encontre a definição', + 'tool.title.findReferences': 'Encontre referências', + 'tool.title.findImplementation': 'Encontre implementação', + 'tool.title.hoverSymbol': 'Símbolo de inspeção', + 'tool.title.spawnTeammate': 'Criar companheiro de equipe', + 'tool.title.createTeamTask': 'Criar tarefa de equipe', + 'tool.title.getTeamTask': 'Ler tarefa da equipe', + 'tool.title.updateTeamTask': 'Atualizar tarefa da equipe', + 'tool.title.listTeamTasks': 'Listar tarefas da equipe', + 'tool.title.waitAgent': 'Aguarde o subagente', + 'detail.recordedResult': 'Resultado registrado', + 'detail.empty': 'Nenhum resultado', + 'detail.none': 'Nenhum', + 'detail.yes': 'Sim', + 'detail.no': 'Não', + 'detail.moreInInspect': 'Mais {count} itens disponíveis no Inspecionar', + 'detail.status.running': 'Correndo', + 'detail.status.idle': 'Inativo', + 'detail.status.ready': 'Pronto', + 'detail.status.inactive': 'Inativo', + 'detail.status.provisioning': 'Provisionamento', + 'detail.status.failed': 'Falha', + 'detail.status.completed': 'Concluído', + 'detail.status.deleted': 'Excluído', + 'detail.status.killed': 'Cancelado', + 'detail.status.accepted': 'Aceito', + 'detail.status.queued': 'Na fila', + 'detail.status.exited': 'Saiu', + 'detail.field.id': 'ID', + 'detail.field.revision': 'Revisão', + 'detail.field.platform': 'Plataforma', + 'detail.field.provider': 'Provedor', + 'detail.field.model': 'Modelo', + 'detail.field.role': 'Função', + 'detail.field.context': 'Contexto', + 'detail.field.owner': 'Proprietário', + 'detail.field.ready': 'Pronto', + 'detail.field.dependencies': 'Dependências', + 'detail.field.writeScopes': 'Escrever escopos', + 'detail.field.warnings': 'Avisos', + 'detail.field.diagnostics': 'Diagnóstico', + 'detail.field.methods': 'Métodos', + 'detail.field.inputSchema': 'Esquema de entrada', + 'detail.field.outputSchema': 'Esquema de saída', + 'detail.field.currentPackage': 'Pacote atual', + 'detail.field.nextPackage': 'Próximo pacote', + 'detail.field.latestRun': 'Última execução', + 'detail.field.packages': 'Pacotes', + 'detail.field.registrations': 'Inscrições', + 'detail.field.props': 'Adereços', + 'detail.field.data': 'Dados', + 'detail.field.source': 'Fonte', + 'detail.field.content': 'Conteúdo', + 'detail.field.message': 'Mensagem', + 'detail.field.messageId': 'ID da mensagem', + 'detail.field.root': 'Raiz', + 'detail.field.pid': 'ID do processo', + 'detail.field.type': 'Tipo', + 'detail.field.time': 'Hora', + 'detail.field.seq': 'Sequência de eventos', + 'detail.field.turn': 'Turno', + 'detail.field.step': 'Passo', + 'detail.field.callId': 'ID de chamada', + 'detail.field.agents': 'Agentes iniciados', + 'detail.field.result': 'Resultado', + 'detail.field.parent': 'Pai', + 'detail.field.depth': 'Profundidade', + 'detail.field.exitCode': 'Código de saída', + 'detail.field.signal': 'Sinal', + 'detail.field.previousStatus': 'Estado anterior', + 'detail.field.agent': 'ID do agente', + 'detail.field.job': 'ID do trabalho', + 'detail.field.task': 'Tarefa', + 'detail.field.processGroup': 'Grupo de processos', + 'detail.field.availability': 'Disponibilidade', + 'detail.field.bestMatch': 'Melhor correspondência', + 'detail.field.target': 'Evento alvo', + 'detail.field.surface': 'Status do registro', + 'detail.agents.count': '{count} agentes', + 'detail.jobs.count': '{count} trabalhos em segundo plano', + 'detail.terminals.count': '{count} terminais', + 'detail.tasks.count': '{count} tarefas de equipe', + 'detail.tasks.nextPage': 'Mais tarefas disponíveis; o próximo cursor é {cursor}', + 'detail.locations.count': '{count} locais', + 'detail.location': 'Linha {line}, coluna {column}', + 'detail.receipt.delivered': 'Mensagem entregue', + 'detail.receipt.interrupt': 'Interrupção solicitada', + 'detail.receipt.started': 'Iniciado', + 'detail.receipt.cancel': 'Cancelamento solicitado', + 'detail.receipt.alreadyFinished': 'Já terminei', + 'detail.receipt.signal': 'Sinal entregue', + 'detail.receipt.closed': 'Fechado', + 'detail.receipt.closing': 'Fechando', + 'detail.wait.noProgress': 'Nenhum subagente ativo', + 'detail.wait.title': 'Atividade do subagente', + 'detail.wait.timeout': 'A espera expirou', + 'detail.wait.changed': 'Alteração detectada', + 'detail.agent.reply': 'Resposta do agente', + 'detail.models.title': 'Modelos disponíveis', + 'detail.output.lines': 'Linhas {begin}–{end} de {total}', + 'detail.output.truncated': 'Saída truncada', + 'detail.providers.count': '{count} inspecionar provedores', + 'detail.plugins.count': '{count} plug-ins dinâmicos', + 'detail.workflow.script': 'Roteiro de fluxo de trabalho', + 'detail.ralph.reportedComplete': 'Conclusão relatada pelo trabalhador', + 'detail.ralph.reportedBlocker': 'Trabalhador relatou um bloqueador', + 'detail.ralph.limit': 'Limite de rodadas atingido', + 'detail.report.nextSteps': 'Trabalho restante', + 'detail.trace.replacedBy': 'Substituído por', + 'detail.trace.replacementChain': 'Cadeia de substituição', + 'detail.trace.replaces': 'Eventos substituídos', + 'detail.trace.sources': 'Eventos de origem', + 'detail.trace.derived': 'Eventos derivados', + 'detail.trace.ancestors': 'Sessões ancestrais', + 'detail.trace.descendants': 'Sessões de descendentes', + 'detail.matches.count': '{count} correspondências', + 'detail.matches.capped': 'Limite de resultados atingido; restringir a busca por mais', + 'detail.event.neighbors': 'Eventos circundantes', + 'todo.completed': '{done}/{total} concluído', + 'command.attachmentsUnsupported': '/{command} não aceita anexos; remova-os primeiro', + 'ask.rowTitle': 'Faça uma pergunta', + 'ask.waiting': 'esperando', + 'ask.pending': 'continuou; resposta ainda disponível', + 'ask.pendingDetail': 'Essas questões pendentes permanecem respondidas pelo compositor.', + 'ask.reopen': 'Resposta', + 'ask.review': 'Ver respostas', + 'ask.closed': 'fechado', + 'ask.closedDetail': 'Esta questão está encerrada; seu resultado está na conversa abaixo.', + 'ask.cancelled': 'cancelado', + 'ask.cancelledDetail': 'Este conjunto de perguntas foi cancelado antes do envio das respostas.', + 'ask.interrupted': 'interrompido', + 'ask.interruptedDetail': 'Este conjunto de perguntas foi interrompido antes do envio das respostas.', + 'ask.answered': '{answered}/{total} respondeu', + 'ask.skipped': 'Não respondido', + 'bash.running': 'Correndo', + 'bash.failed': 'Falha', + 'bash.stopped': 'Parado', + 'row.running': 'Correndo', + 'row.preparing': 'Preparando chamada de ferramenta', + 'row.failed': 'Falha', + 'row.stopped': 'Parado', + 'row.commandArguments': 'Argumentos de comando', + 'row.input': 'IN', + 'row.output': 'OUT', + 'row.inspect': 'Inspecionar', + 'row.imageSize': '{width}x{height}px, {bytes} bytes', + 'row.imageSizeDownscaled': '{width}x{height}px (reduzido de {originalWidth}x{originalHeight}px), {bytes} bytes', + 'tool.title.search': 'Pesquisar', + 'tool.title.read': 'Leia', + 'tool.title.bash': 'Bash', + 'tool.title.write': 'Escreva', + 'tool.title.edit': 'Editar', + 'tool.title.code': 'Código', + 'tool.title.generic': 'Chamada de ferramenta', + 'tool.title.inspect': 'Consultar ambiente Cordis', + 'tool.title.runCordis': 'Execute o plug-in Cordis', + 'tool.title.stopCordis': 'Parar o plug-in Cordis', + 'tool.title.removeCordis': 'Remover plug-in Cordis', + 'tool.title.pwsh': 'Pwsh', + 'tool.title.readImage': 'Ler imagem', + 'tool.title.grep': 'Grep', + 'tool.title.glob': 'globo', + 'tool.title.webSearch': 'Pesquisar', + 'tool.title.webFetch': 'Buscar', + 'tool.autoReviewRejected': 'Rejeitado por Auto review', + 'tool.autoReviewNotExecuted': 'A ferramenta não foi executada. Motivo: {reason}', + 'tool.autoReviewReasonFallback': 'Auto review não autorizou esta ação', + 'diff.collapseAria': 'Recolher diferença', + 'diff.expandAria': 'Expanda mais {count} linhas de diferenças', + 'diff.expandRest': '… mais {count} linhas', + 'read.window': 'Mostrando {shown} de {total} linhas', + 'read.collapseAria': 'Recolher conteúdo', + 'read.expandAria': 'Expanda mais {count} linhas', + 'read.expandRest': '… mais {count} linhas', + 'search.paths': '{shown} caminhos', + 'search.paths.truncated': 'Mostrando {shown} de {total} caminhos', + 'search.matches': '{shown} correspondências · {files} arquivos', + 'search.matches.truncated': 'Mostrando {shown} de {total} correspondências · {files} arquivos', + 'search.noResults': 'Nenhum resultado', + 'search.collapseAria': 'Recolher resultados', + 'search.expandAria': 'Expanda mais {count} linhas de resultados', + 'search.expandRest': '… mais {count} linhas', + 'web.noResults': 'Nenhum resultado encontrado', + 'web.sourcesTruncated': 'Lista de fontes truncada', + 'web.http': 'HTTP', + 'web.contentTruncated': 'Conteúdo truncado', + 'details.running': 'Correndo…', + 'queue.count': '{n} mensagens na fila', + 'queue.sending': 'Enviando…', + 'queue.image': 'Imagem de mensagem na fila', + 'queue.file': 'Arquivo na fila {name}', + 'queue.edit': 'Editar mensagem na fila', + 'queue.edit.unsupported': 'Contém conteúdo não textual; a edição ainda não é suportada', + 'queue.save': 'Salvar mensagem na fila', + 'queue.cancelEdit': 'Cancelar edição', + 'queue.remove': 'Remover mensagem na fila', + 'queue.steer': 'Orientar com mensagem da fila', + 'queue.steer.unavailable': 'A direção está disponível apenas enquanto o agente está em execução', + 'error.sessionInUse': 'Esta sessão já está em uso, possivelmente por outra instância DSH em execução (como dsh web ou aplicativo de desktop). Saia de outras instâncias DSH em execução e tente novamente.', + 'queue.editFailed': 'Falha na edição: esta mensagem pode já ter começado a ser enviada.', + 'queue.removeFailed': 'Falha na remoção: esta mensagem pode já ter começado a ser enviada.', + 'queue.steerFailed': 'A direção falhou. Tente novamente.', + 'terminal.signal': 'sinal {signal}', + 'terminal.exitCode': 'código de saída {code}', + 'terminal.noExitCode': 'sem código de saída', + 'terminal.commandLine': 'Linha de comando {n}', + 'terminal.running': 'Correndo', + 'terminal.failed': 'Falha', + 'terminal.done': 'Concluído', + 'terminal.noOutput': 'Sem saída', + 'terminal.collapseAria': 'Recolher saída', + 'terminal.expandAria': 'Expanda as {n} linhas de saída restantes', + 'terminal.expandRest': '… mais {n} linhas', + // The Host terminal_send presenter has no locale seat; keep its fallbacks + // aligned with these English values. + 'terminal.sendInput': '(enviar entrada)', + 'terminal.session': 'Terminal {sessionId}', +} satisfies Record diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css index cf92656162..1f0380680c 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css @@ -198,6 +198,9 @@ z-index: 1; display: flex; gap: 16px 20px; + width: 100%; + min-width: 0; + box-sizing: border-box; align-items: flex-start; flex-wrap: wrap; margin-top: 10px; @@ -212,12 +215,20 @@ min-height: 25px; } -/* The 16px text line retains 9px below it. The active bar reaches 1px past - the tab box to sit over the header rule. */ +/* The 16px text line retains 9px below it. The press target extends 10px + above the text into the strip's top margin and 10px to each side, matching + the inline padding of the Sidebar tab chips (ui-dockkit `.tab`); negative + margins keep the layout box at its 25px row and text width. The active bar + spans the text and reaches 1px past the tab box to sit over the header + rule. */ .tab { + --dsh-tab-hit-inline: 10px; + position: relative; - min-height: 25px; - padding: 0 0 9px; + box-sizing: border-box; + min-height: 35px; + margin: -10px calc(-1 * var(--dsh-tab-hit-inline)) 0; + padding: 10px var(--dsh-tab-hit-inline) 9px; border: none; background: transparent; font-size: 13px; @@ -230,9 +241,9 @@ .tab::after { content: ''; position: absolute; - right: 0; + right: var(--dsh-tab-hit-inline); bottom: -1px; - left: 0; + left: var(--dsh-tab-hit-inline); height: 2px; border-radius: 2px; background: transparent; @@ -262,6 +273,10 @@ color: var(--dsw-alias-label-secondary); } +.viewsMoreActive { color: var(--dsw-alias-state-business-primary); } + +.tabLabel { display: block; max-width: 88px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .viewsMore[aria-expanded='true'] { background: var(--dsw-alias-interactive-bg-hover); } diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx b/packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx index f4ef1f342a..3089b083df 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx @@ -1,6 +1,6 @@ /** Strict per-session header/body content inserted into the resident conversation layout. */ -import { useState } from 'react' +import { useLayoutEffect, useMemo, useRef, useState } from 'react' import clsx from 'clsx' import { Button, IconEllipsisOutlineRegular, Menu, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' import type { SessionListState, SessionSummary } from '@deepseek-ai/dsh-api-session-controller/client' @@ -22,17 +22,17 @@ export type ConversationSessionHeaderProps = ConversationSessionHeaderSlotProps type ViewsMoreMenuProps = Omit & Pick - & { readonly label: string } + & { readonly label: string; readonly current: boolean } /** The View tab row's More launcher; its menu rows come from `conversation.session.header.views.actions`. */ -function ViewsMoreMenu({ label, entries, onOpen, onPinnedChange, renderSlot }: ViewsMoreMenuProps) { +function ViewsMoreMenu({ label, current, entries, onOpen, onPinnedChange, renderSlot }: ViewsMoreMenuProps) { const [open, setOpen] = useState(false) const onDismiss = (): void => { setOpen(false) } return ( - @@ -93,6 +93,31 @@ export function ConversationSessionHeader({ const unpinned = useEntryPins(ids => ids) const hasEntryActions = useEntryActions(registered => registered) const showTabs = !hideChrome && tabs.length > 1 + const tabRow = useRef(null) + const [visibleViewLimit, setVisibleViewLimit] = useState(1) + // Observe only the available row width; individual tabs are not measured. + useLayoutEffect(() => { + const row = tabRow.current + if (row === null || !hasEntryActions) return + const updateLimit = (): void => { + setVisibleViewLimit(Math.max(1, Math.floor((row.clientWidth - 48) / 120))) + } + updateLimit() + if (typeof ResizeObserver === 'undefined') return + const observer = new ResizeObserver(updateLimit) + observer.observe(row) + return () => { observer.disconnect() } + }, [showTabs, hasEntryActions]) + const { visibleTabs, moreTabs } = useMemo(() => { + const preferred = [ + ...tabs.filter(tab => tab.id === FIXED_VIEW_ID), + ...tabs.filter(tab => tab.id !== FIXED_VIEW_ID && !unpinned.includes(tab.id)), + ] + const visibleTabs = hasEntryActions ? preferred.slice(0, visibleViewLimit) : preferred + const visibleIds = new Set(visibleTabs.map(tab => tab.id)) + return { visibleTabs, moreTabs: tabs.filter(tab => !visibleIds.has(tab.id)) } + }, [tabs, unpinned, hasEntryActions, visibleViewLimit]) + const currentInMore = moreTabs.some(tab => tab.id === active?.id) return ( <>
@@ -169,10 +194,9 @@ export function ConversationSessionHeader({ {showTabs && ( // data-conversation-tabs: marks the tab strip, which the window-chrome // geometry and the browser coverage lane anchor on. -
+
- {tabs.filter(viewTab => viewTab.id === FIXED_VIEW_ID || viewTab.id === active?.id - || !unpinned.includes(viewTab.id)).map(viewTab => ( + {visibleTabs.map(viewTab => ( ))}
{hasEntryActions && ( viewTab.id !== FIXED_VIEW_ID) - .map(viewTab => ({ id: viewTab.id, label: viewTab.label, pinned: !unpinned.includes(viewTab.id) }))} + label={currentInMore && active !== undefined ? t('session.moreViewsCurrent', { name: active.label }) : t('session.moreViews')} + current={currentInMore} + entries={moreTabs.map(viewTab => ({ + id: viewTab.id, label: viewTab.label, + pinned: !unpinned.includes(viewTab.id), active: viewTab.id === active?.id, + }))} onOpen={selectView} onPinnedChange={setPinned} renderSlot={renderSlot} diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx index 80d04e9b80..fd0d9190ec 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx @@ -48,6 +48,7 @@ export const InputBar = memo(function InputBar({ retryFileUpload, toggleCommandMenu, stop, t, renderSlot, useBusyEnter, useFileUploads, useNotices, useLexicon, useMenuLauncher, useStopShortcut, + useAccess, useProjection, sessionId, variant, disabled: inert = false, blocked, workspacePickerOpen = false, onRequestWorkspace, placeholder, accessory, @@ -56,6 +57,13 @@ export const InputBar = memo(function InputBar({ const notice = useNotices(s => s) const busyEnter = useBusyEnter(s => s) const stopKeys = useStopShortcut(keys => keys) + // A page whose access snapshot excludes prompting, stopping, or uploading to this Session is offered + // no Send, no Stop, or no file intake rather than a disabled control. `useAccess` is undefined only + // without a Session, where the bar is an inert placeholder with no keyboard, stop, or file intake. + const promptDenied = useAccess('session/prompt') === false + const cancelDenied = useAccess('session/cancel') === false + const uploadDenied = useAccess('fileUploads/upload') === false + const stopAction = cancelDenied ? undefined : stop void useLexicon // hook seat stays bound by the inject compartment; text-ref decoration rides the shell's editor transforms const commandMenuOpen = useMenuLauncher(source => source === 'command') const [activity, setActivity] = useState(false) @@ -214,7 +222,8 @@ export const InputBar = memo(function InputBar({ // The host enforces the same image limits at submit for callers that bypass // this composer. const intakeFiles = useCallback((files: readonly File[], directories?: ReadonlySet): void => { - if (locked || machineBusy || (subagent !== null && !continuable) || addFiles === undefined || files.length === 0) return + if (uploadDenied || locked || machineBusy || (subagent !== null && !continuable) + || addFiles === undefined || files.length === 0) return const rejected = ((): string | null => { // Child prompts support images, but not generic file receipts or the // Desktop path-reference fallback. Refuse mixed batches before intake. @@ -240,9 +249,9 @@ export const InputBar = memo(function InputBar({ return addFiles(files, directories) })() if (rejected !== null) showToast(rejected) - }, [locked, machineBusy, subagent, continuable, addFiles, attachments, imageLimits, showToast, t]) + }, [uploadDenied, locked, machineBusy, subagent, continuable, addFiles, attachments, imageLimits, showToast, t]) - const canAcceptDrop = (subagent === null || continuable) && !locked && !machineBusy && addFiles !== undefined + const canAcceptDrop = !uploadDenied && (subagent === null || continuable) && !locked && !machineBusy && addFiles !== undefined const fileInputRef = useRef(null) const onPickFiles = (e: ChangeEvent): void => { @@ -331,7 +340,7 @@ export const InputBar = memo(function InputBar({ // exposes Stop independently. const primaryStops = running && subagent === null && (empty || blocked !== undefined) // Disabled native buttons may omit mouseleave; their tooltip must close from state. - const primaryDisabled = primaryStops ? stop === undefined : empty || disabled || machineBusy || uploadsPending + const primaryDisabled = primaryStops ? stopAction === undefined : empty || disabled || machineBusy || uploadsPending const interruptible = running && continuable const primarySubmitMode = resolveSubmitMode(busyEnter, running, 'enter', steeringAvailable) const plainMessageDraft = !empty && input?.phase === 'plain' && !draft.trimStart().startsWith('/') @@ -342,7 +351,7 @@ export const InputBar = memo(function InputBar({ : t('input.send') const onPrimary = (): void => { if (primaryStops) { - stop?.() + stopAction?.() return } if (keyboard === undefined) return // absent machine: the button is disabled @@ -467,7 +476,7 @@ export const InputBar = memo(function InputBar({ ref={fileInputRef} type="file" multiple - disabled={subagent !== null} + disabled={!canAcceptDrop || subagent !== null} hidden onChange={onPickFiles} /> @@ -489,15 +498,15 @@ export const InputBar = memo(function InputBar({ {input === undefined || sessionId === undefined ? null :
{renderSlot('conversation.input.activity', { locked, onActiveChange: setActivity })}
} - {interruptible && ( - + {interruptible && !cancelDenied && ( + )} - - - + {(primaryStops ? cancelDenied : promptDenied) ? null : ( + + + + )}
diff --git a/packages/client/ui-conversation/src/client/view-pins.ts b/packages/client/ui-conversation/src/client/view-pins.ts index aebf97470a..d3d4e1f408 100644 --- a/packages/client/ui-conversation/src/client/view-pins.ts +++ b/packages/client/ui-conversation/src/client/view-pins.ts @@ -1,11 +1,9 @@ -/** Persisted pin choices for the Conversation View tabs. */ +/** In-memory pin choices for the Conversation View tabs. */ import { createSnapshotStore, type ObservableSnapshot } from '@deepseek-ai/dsh-client-store' /** View whose tab always shows and never enters the customization list. */ export const FIXED_VIEW_ID = 'chat' -/** Root-level key shared with the slot layer's future per-list preferences; Sessions share one list. */ -const STORAGE_KEY = 'dsh.slot.manual-order.conversation.view.v1' const NO_VIEWS: readonly string[] = [] /** Unpinned View ids and their only mutation. */ @@ -13,30 +11,24 @@ export interface ViewPins { /** Unpinned View ids; ids of Views that are not registered stay listed. */ readonly unpinned: ObservableSnapshot /** - * Show or hide one View tab; the fixed View is ignored. + * Pin or unpin one View tab; the fixed View is ignored. A pinned tab still + * enters More when the header's width budget cannot hold it. * @param id - View id. - * @param pinned - Whether the tab shows in the Conversation header. + * @param pinned - Whether the tab is manually pinned. */ readonly setPinned: (id: string, pinned: boolean) => void } -/** Stored content is unchecked; anything but a string array leaves every View pinned. */ -function unpinnedViews(value: unknown): readonly string[] { - if (typeof value !== 'object' || value === null || !('unpinned' in value)) return NO_VIEWS - const { unpinned } = value - return Array.isArray(unpinned) && unpinned.every((id): id is string => typeof id === 'string') ? unpinned : NO_VIEWS -} - /** - * Create the persisted View pin list; the snapshot keeps its reference until the list changes. - * @returns Validated unpinned View ids and the pin mutation. + * Create the in-memory View pin list; the snapshot keeps its reference until the list changes. + * @returns Unpinned View ids and the pin mutation. */ export function createViewPins(): ViewPins { - const store = createSnapshotStore({ unpinned: NO_VIEWS }, { persist: { name: STORAGE_KEY } }) + const store = createSnapshotStore<{ readonly unpinned: readonly string[] }>({ unpinned: NO_VIEWS }) return { - unpinned: { getSnapshot: () => unpinnedViews(store.getSnapshot()), subscribe: listener => store.subscribe(listener) }, + unpinned: { getSnapshot: () => store.getSnapshot().unpinned, subscribe: listener => store.subscribe(listener) }, setPinned: (id, pinned) => { - const current = unpinnedViews(store.getSnapshot()) + const current = store.getSnapshot().unpinned if (id === FIXED_VIEW_ID || pinned !== current.includes(id)) return store.set({ unpinned: pinned ? current.filter(entry => entry !== id) : [...current, id] }) }, diff --git a/packages/client/ui-conversation/tests/apply-wiring.client.spec.tsx b/packages/client/ui-conversation/tests/apply-wiring.client.spec.tsx index fa7f2fc4ef..a7b0bfdbdf 100644 --- a/packages/client/ui-conversation/tests/apply-wiring.client.spec.tsx +++ b/packages/client/ui-conversation/tests/apply-wiring.client.spec.tsx @@ -44,7 +44,7 @@ function entry( } describe('target-neutral Conversation apply wiring', () => { - it('hands every Session header the same persisted View pins', async () => { + it('hands every Session header the same in-memory View pins without restoring saved choices', async () => { localStorage.setItem('dsh.slot.manual-order.conversation.view.v1', JSON.stringify({ unpinned: ['trajectory'] })) try { const b = await bench() @@ -56,10 +56,10 @@ describe('target-neutral Conversation apply wiring', () => { const inject = header.inject as (sessionId: SessionId) => PinHooks const injected = inject(SID) const first = injected.hooks.entryPins - expect(first.getSnapshot()).toEqual(['trajectory']) + expect(first.getSnapshot()).toEqual([]) expect(inject('session-2' as SessionId).hooks.entryPins).toBe(first) injected.setPinned('timeline', false) - expect(first.getSnapshot()).toEqual(['trajectory', 'timeline']) + expect(first.getSnapshot()).toEqual(['timeline']) const actions = injected.hooks.entryActions const changed = vi.fn() actions.subscribe(changed) diff --git a/packages/client/ui-conversation/tests/header-styles.client.spec.ts b/packages/client/ui-conversation/tests/header-styles.client.spec.ts index c3abd2bcf4..3c964950d0 100644 --- a/packages/client/ui-conversation/tests/header-styles.client.spec.ts +++ b/packages/client/ui-conversation/tests/header-styles.client.spec.ts @@ -8,8 +8,10 @@ describe('conversation header geometry', () => { it('keeps the single-line header at the shared 76px height', () => { expect(css.match(/\.header\s*\{([^}]*)\}/)?.[1]).toContain('min-height: 76px') expect(css.match(/\.viewTabs\s*\{([^}]*)\}/)?.[1]).toContain('min-height: 25px') - expect(css.match(/\.tab\s*\{([^}]*)\}/)?.[1]).toContain('min-height: 25px') - expect(css.match(/\.tab\s*\{([^}]*)\}/)?.[1]).toContain('padding: 0 0 9px') + // The tab's 35px press target sits 10px above its 25px row through a negative top margin. + expect(css.match(/\.tab\s*\{([^}]*)\}/)?.[1]).toContain('min-height: 35px') + expect(css.match(/\.tab\s*\{([^}]*)\}/)?.[1]).toContain('margin: -10px calc(-1 * var(--dsh-tab-hit-inline)) 0') + expect(css.match(/\.tab\s*\{([^}]*)\}/)?.[1]).toContain('padding: 10px var(--dsh-tab-hit-inline) 9px') }) it('uses 32px between view labels without changing the action gap', () => { diff --git a/packages/client/ui-conversation/tests/input-bar.client.spec.tsx b/packages/client/ui-conversation/tests/input-bar.client.spec.tsx index c84953db02..a6968a7055 100644 --- a/packages/client/ui-conversation/tests/input-bar.client.spec.tsx +++ b/packages/client/ui-conversation/tests/input-bar.client.spec.tsx @@ -106,6 +106,8 @@ interface BenchOptions { attachments?: readonly ComposerAttachment[] /** Upload states served for file-kind drafts (absent = every file is ready). */ fileUploads?: DraftFileUploads + /** Endpoints the Session's access snapshot excludes (absent = every endpoint is allowed). */ + deniedEndpoints?: readonly string[] addFiles?: (files: readonly File[], directories?: ReadonlySet) => string | null commandMenuOpen?: boolean busyEnter?: 'queue' | 'steer' @@ -193,6 +195,7 @@ function bench(over?: BenchOptions) { items: [], archivedSessionIds: [], pinnedSessionIds: [], state: 'idle', phase: 'ready', error: null, })), ...allowAllAccess, + useAccess: endpoint => !(over?.deniedEndpoints ?? []).includes(endpoint), useProjection: ((key: string, selector?: (v: unknown) => unknown) => (selector ?? (v => v))(key === 'plan' ? over?.plan @@ -708,6 +711,23 @@ describe('image draft rail', () => { } }) + it('keeps text editable but refuses attachment-slot, picker and paste intake without permission', () => { + const added = vi.fn(() => null) + const result = bench({ deniedEndpoints: ['fileUploads/upload'], addFiles: added, draft: 'text stays editable' }) + const attachment = attachmentOwner(result.slotCalls) + expect(attachment.canAcceptDrop).toBe(false) + const file = new File(['note'], 'note.txt') + attachment.onAddFiles([file]) + const picker = result.view.container.querySelector('input[type=file]')! + expect(picker.disabled).toBe(true) + fireEvent.change(picker, { target: { files: [file] } }) + fireEvent.paste(result.textarea, { clipboardData: { + items: [{ kind: 'file', type: 'text/plain', getAsFile: () => file }], getData: () => '', + } }) + expect(added).not.toHaveBeenCalled() + expect(result.textarea.getAttribute('contenteditable')).toBe('true') + }) + it('announces a rejected attachment-slot intake through the same toast', () => { const addFiles = vi.fn(() => '图片读取服务不可用') const result = bench({ addFiles }) @@ -1197,6 +1217,41 @@ describe('running and lock semantics', () => { expect(sink).toHaveBeenCalledWith('直接插话', [], 'steer', expect.any(AbortSignal)) }) + it('a running composer whose access excludes prompting and stopping offers neither Stop nor Send', () => { + const { view, stop } = bench({ + running: true, + // The bar is handed a live Stop handler all the same, so what withholds the action is the access + // answer and not a missing face. + deniedEndpoints: ['session/prompt', 'session/cancel'], + blocked: { reason: '只读查看:本页由 owner 分享,无法发送消息' }, + placeholder: '只读查看:本页由 owner 分享,无法发送消息', + }) + expect(view.container.querySelector('button[aria-label="停止生成"]')).toBeNull() + expect(view.container.querySelector('button[aria-label="发送消息"]')).toBeNull() + expect(stop).not.toHaveBeenCalled() + }) + + it('a running composer whose access excludes only prompting keeps Stop but offers no busy Send', () => { + const empty = bench({ running: true, deniedEndpoints: ['session/prompt'] }) + expect(empty.view.getByLabelText('停止生成')).toBeTruthy() + cleanup() + // A draft would make the primary action the busy Send, which the snapshot excludes. + const drafted = bench({ running: true, deniedEndpoints: ['session/prompt'], draft: '排到下一轮' }) + expect(drafted.view.container.querySelector('button[aria-label="排队发送"]')).toBeNull() + }) + + it('an idle composer whose access excludes prompting offers no Send', () => { + const { view } = bench({ deniedEndpoints: ['session/prompt'], draft: 'hello' }) + expect(view.container.querySelector('button[aria-label="发送消息"]')).toBeNull() + }) + + it('a composer whose access has no answer yet keeps its ordinary Send', () => { + const { view, props } = bench({ draft: 'hello' }) + // Without a bound Session the Session-scope Hook answers undefined, which denies nothing. + view.rerender( undefined} />) + expect(view.getByLabelText('发送消息')).toBeTruthy() + }) + it('running Cmd/Ctrl+Enter uses the opposite of the busy-state Enter preference', () => { const meta = bench({ running: true, busyEnter: 'steer', draft: '排到下一轮' }) fireEvent.keyDown(meta.textarea, { key: 'Enter', metaKey: true }) @@ -1959,6 +2014,21 @@ describe('command launcher chrome and control seats', () => { expect(launcher.getAttribute('aria-expanded')).toBe('true') }) + it('follows the access answers over a running Session, and restores the actions when they return', () => { + const { view, props } = bench({ running: true }) + // While the page may stop the running Session, the primary action is its Stop. + expect(view.getByLabelText('停止生成')).toBeTruthy() + + // The access snapshot now excludes the Session's writes: the bar offers neither Stop nor Send. + view.rerender( false} />) + expect(view.queryByLabelText('停止生成')).toBeNull() + expect(view.queryByLabelText('发送消息')).toBeNull() + + // The answers return: the bar offers the action the page holds again. + view.rerender() + expect(view.getByLabelText('停止生成')).toBeTruthy() + }) + it('opening the command menu from the button puts the keyboard in the editor first', () => { const toggleCommandMenu = vi.fn() const { view, textarea } = bench({ toggleCommandMenu }) diff --git a/packages/client/ui-conversation/tests/skeleton.client.spec.tsx b/packages/client/ui-conversation/tests/skeleton.client.spec.tsx index b278797d5d..bda16c8a08 100644 --- a/packages/client/ui-conversation/tests/skeleton.client.spec.tsx +++ b/packages/client/ui-conversation/tests/skeleton.client.spec.tsx @@ -85,6 +85,7 @@ function fireResize(el: Element): void { afterEach(() => { cleanup() + vi.restoreAllMocks() vi.unstubAllGlobals() resizeObservers.length = 0 }) @@ -144,6 +145,8 @@ function mount( unpinnedViews?: readonly string[] /** Whether the View actions list has registrations. */ entryActions?: boolean + /** Isolate header capability checks from the composer's required layout observers. */ + headerOnly?: boolean } = {}, ) { const sessionId = 'sessionId' in options ? options.sessionId : SID @@ -340,6 +343,7 @@ function mount( const renderFactorySlot = ((_name: string, input: ConversationContentInputProps, factoryOptions?: { slots?: Record ReactNode> }) => { + if (options.headerOnly) return null const common: ConversationViewsProps = { sessionId: SID, SessionProvider, @@ -681,7 +685,30 @@ describe('ConversationRoot resident composer', () => { expect(b.view.getByRole('tablist').hasAttribute('data-conversation-tabs')).toBe(true) }) - it('keeps the selected View tab while it is unpinned and hides it once another View is selected', () => { + it('keeps Chat outside at the narrowest width even when it registers after other Views', () => { + vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(180) + const viewTabs = ['trajectory', 'timeline', 'chat', 'notes'].map(id => ({ id, label: id })) + const b = mount(sessionSnapshotOf(), undefined, undefined, { viewTabs, entryActions: true }) + expect(within(b.view.getByRole('tablist')).getAllByRole('tab').map(tab => tab.textContent)).toEqual(['chat']) + fireEvent.click(b.view.getByRole('button', { name: zh['session.moreViews'] })) + expect(b.actionsOwner().entries.map(entry => entry.id)).toEqual(['trajectory', 'timeline', 'notes']) + act(() => { b.actionsOwner().onOpen('trajectory') }) + expect(b.view.getByRole('tab', { name: 'chat' })).toBeTruthy() + expect(b.view.queryByRole('tab', { name: 'trajectory' })).toBeNull() + expect(b.actionsOwner().entries.find(entry => entry.id === 'trajectory')?.active).toBe(true) + }) + + it('calculates the initial tab budget when ResizeObserver is unavailable', () => { + vi.stubGlobal('ResizeObserver', undefined) + vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(300) + const viewTabs = ['chat', 'trajectory', 'timeline'].map(id => ({ id, label: id })) + const b = mount(sessionSnapshotOf(), undefined, undefined, { viewTabs, entryActions: true, headerOnly: true }) + expect(within(b.view.getByRole('tablist')).getAllByRole('tab').map(tab => tab.textContent)).toEqual(['chat', 'trajectory']) + fireEvent.click(b.view.getByRole('button', { name: zh['session.moreViews'] })) + expect(b.actionsOwner().entries.map(entry => entry.id)).toEqual(['timeline']) + }) + + it('keeps an unpinned View selected without exposing its tab', () => { const viewTabs: ViewTab[] = [ { id: 'chat', label: 'Chat' }, { id: 'trajectory', label: 'Trajectory' }, @@ -691,7 +718,7 @@ describe('ConversationRoot resident composer', () => { act(() => { b.store.actions.setView('trajectory') }) const tabs = (): HTMLElement[] => within(b.view.getByRole('tablist')).getAllByRole('tab') expect(tabs().map(tab => [tab.textContent, tab.getAttribute('aria-selected')])).toEqual([ - ['Chat', 'false'], ['Trajectory', 'true'], ['Timeline', 'false'], + ['Chat', 'false'], ['Timeline', 'false'], ]) expect(b.view.getByTestId('view-trajectory')).toBeTruthy() act(() => { b.store.actions.setView('chat') }) @@ -708,6 +735,75 @@ describe('ConversationRoot resident composer', () => { expect(b.view.queryByRole('menu')).toBeNull() }) + it('reuses the width budget on View roster and selection changes without a window resize listener', () => { + let width = 480 + const readWidth = vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => width) + const viewTabs: ViewTab[] = [ + { id: 'chat', label: 'Chat' }, + { id: 'trajectory', label: 'Trajectory' }, + { id: 'timeline', label: 'Timeline' }, + { id: 'inspector', label: 'Inspector' }, + ] + const b = mount(sessionSnapshotOf(), undefined, undefined, { viewTabs, entryActions: true }) + const external = () => within(b.view.getByRole('tablist')).getAllByRole('tab').map(tab => tab.textContent) + expect(external()).toEqual(['Chat', 'Trajectory', 'Timeline']) + fireEvent.click(b.view.getByRole('button', { name: zh['session.moreViews'] })) + expect(b.actionsOwner().entries.map(entry => entry.id)).toEqual(['inspector']) + act(() => { b.actionsOwner().onOpen('inspector') }) + expect(external()).toEqual(['Chat', 'Trajectory', 'Timeline']) + expect(b.actionsOwner().entries.find(entry => entry.id === 'inspector')?.active).toBe(true) + expect(b.view.getByRole('button', { name: '更多会话视图 · Inspector' })).toBeTruthy() + const reads = readWidth.mock.calls.length + width = 1000 + fireEvent(window, new Event('resize')) + act(() => { b.store.actions.setView('timeline') }) + expect(readWidth).toHaveBeenCalledTimes(reads) + expect(external()).toEqual(['Chat', 'Trajectory', 'Timeline']) + viewTabs.splice(1, 1) + act(() => { b.store.actions.setView('chat') }) + expect(external()).toEqual(['Chat', 'Timeline', 'Inspector']) + expect(readWidth).toHaveBeenCalledTimes(reads) + expect(b.actionsOwner().entries).toEqual([]) + expect(b.setPinned).not.toHaveBeenCalled() + }) + + it('folds and restores Views on container resize without changing selection, and disconnects on unmount', () => { + let width = 1000 + vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => width) + const viewTabs = ['chat', 'trajectory', 'timeline', 'inspector', 'notes'].map(id => ({ id, label: id })) + const b = mount(sessionSnapshotOf(), undefined, undefined, { viewTabs, entryActions: true }) + const row = b.view.getByRole('tablist').parentElement! + const external = () => within(b.view.getByRole('tablist')).getAllByRole('tab').map(tab => tab.textContent) + expect(external()).toEqual(viewTabs.map(tab => tab.label)) + act(() => { b.store.actions.setView('notes') }) + width = 300 + act(() => { fireResize(row) }) + expect(external()).toEqual(['chat', 'trajectory']) + fireEvent.click(b.view.getByRole('button', { name: '更多会话视图 · notes' })) + expect(b.actionsOwner().entries.map(entry => entry.id)).toEqual(['timeline', 'inspector', 'notes']) + expect(b.actionsOwner().entries.find(entry => entry.id === 'notes')?.active).toBe(true) + width = 1000 + act(() => { fireResize(row) }) + expect(external()).toEqual(viewTabs.map(tab => tab.label)) + expect(b.view.getByRole('tab', { name: 'notes' }).getAttribute('aria-selected')).toBe('true') + expect(b.actionsOwner().entries).toEqual([]) + expect(b.setPinned).not.toHaveBeenCalled() + const observers = resizeObservers.filter(observer => observer.targets.includes(row)) + expect(observers).toHaveLength(1) + b.view.unmount() + expect(observers[0]!.targets).toEqual([]) + }) + + it('keeps all five Views outside when the sampled width budget is sufficient', () => { + vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(1000) + const viewTabs = ['chat', 'trajectory', 'timeline', 'inspector', 'notes'].map(id => ({ id, label: id })) + const b = mount(sessionSnapshotOf(), undefined, undefined, { viewTabs, entryActions: true }) + expect(within(b.view.getByRole('tablist')).getAllByRole('tab').map(tab => tab.textContent)) + .toEqual(viewTabs.map(tab => tab.label)) + fireEvent.click(b.view.getByRole('button', { name: zh['session.moreViews'] })) + expect(b.actionsOwner().entries).toEqual([]) + }) + it('omits More while the View actions list has no registrations', () => { const b = mount(sessionSnapshotOf()) expect(b.view.getByRole('tablist')).toBeTruthy() @@ -726,11 +822,13 @@ describe('ConversationRoot resident composer', () => { fireEvent.click(more) expect(more.getAttribute('aria-expanded')).toBe('true') expect(within(b.view.getByRole('menu')).getByRole('menuitem', { name: 'Header view action' })).toBeTruthy() - expect(b.actionsOwner().entries).toEqual([{ id: 'trajectory', label: 'Trajectory', pinned: false }]) + expect(b.actionsOwner().entries).toEqual([{ id: 'trajectory', label: 'Trajectory', pinned: false, active: false }]) b.actionsOwner().onPinnedChange('trajectory', true) expect(b.setPinned).toHaveBeenCalledExactlyOnceWith('trajectory', true) act(() => { b.actionsOwner().onOpen('trajectory') }) expect(b.view.getByTestId('view-trajectory')).toBeTruthy() + expect(b.view.queryByRole('tab', { name: 'Trajectory' })).toBeNull() + expect(b.actionsOwner().entries).toEqual([{ id: 'trajectory', label: 'Trajectory', pinned: false, active: true }]) fireEvent.click(b.view.getByRole('menuitem', { name: 'Header view action' })) expect(b.view.queryByRole('menu')).toBeNull() expect(more.getAttribute('aria-expanded')).toBe('false') diff --git a/packages/client/ui-conversation/tests/view-pins.client.spec.ts b/packages/client/ui-conversation/tests/view-pins.client.spec.ts index b42e6171ee..fa4b42d53d 100644 --- a/packages/client/ui-conversation/tests/view-pins.client.spec.ts +++ b/packages/client/ui-conversation/tests/view-pins.client.spec.ts @@ -1,5 +1,5 @@ // @vitest-environment jsdom -/** Conversation View pins persist one root list for every Session and ignore Chat. */ +/** Conversation View pins share one in-memory root list for every Session and ignore Chat. */ import { afterEach, describe, expect, it, vi } from 'vitest' import { createViewPins } from '../src/client/view-pins.ts' @@ -7,15 +7,15 @@ const KEY = 'dsh.slot.manual-order.conversation.view.v1' afterEach(() => { localStorage.clear() }) describe('Conversation View pins', () => { - it('persists unpinned View ids and restores them in a new instance', () => { + it('keeps unpinned View ids in memory without restoring them in a new instance', () => { const pins = createViewPins() const initial = pins.unpinned.getSnapshot() expect(initial).toEqual([]) expect(pins.unpinned.getSnapshot()).toBe(initial) pins.setPinned('trajectory', false) pins.setPinned('timeline', false) - expect(JSON.parse(localStorage.getItem(KEY) ?? 'null')).toEqual({ unpinned: ['trajectory', 'timeline'] }) - expect(createViewPins().unpinned.getSnapshot()).toEqual(['trajectory', 'timeline']) + expect(localStorage.getItem(KEY)).toBeNull() + expect(createViewPins().unpinned.getSnapshot()).toEqual([]) pins.setPinned('trajectory', true) expect(pins.unpinned.getSnapshot()).toEqual(['timeline']) }) @@ -34,20 +34,14 @@ describe('Conversation View pins', () => { }) it('keeps ids of Views that are not registered', () => { - localStorage.setItem(KEY, JSON.stringify({ unpinned: ['removed-view'] })) const pins = createViewPins() + pins.setPinned('removed-view', false) pins.setPinned('timeline', false) expect(pins.unpinned.getSnapshot()).toEqual(['removed-view', 'timeline']) }) - it.each([ - ['a non-array list', { unpinned: { timeline: true } }], - ['non-string ids', { unpinned: [null] }], - ['a missing list', { pinned: [] }], - ['null', null], - ['an array root', ['timeline']], - ])('reads %s as no unpinned Views with a stable snapshot', (_name, stored) => { - localStorage.setItem(KEY, JSON.stringify(stored)) + it('ignores a legacy stored list instead of restoring it', () => { + localStorage.setItem(KEY, JSON.stringify({ unpinned: ['trajectory'] })) const pins = createViewPins() const snapshot = pins.unpinned.getSnapshot() expect(snapshot).toEqual([]) diff --git a/packages/client/ui-deepseek-model-label-1017/locale/de.json b/packages/client/ui-deepseek-model-label-1017/locale/de.json new file mode 100644 index 0000000000..97f572f313 --- /dev/null +++ b/packages/client/ui-deepseek-model-label-1017/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek 1017 Modellplakette", + "description": "Fügen Sie den offiziellen DeepSeek Flash-Modellnamen ein temporäres 1017-Release-Abzeichen hinzu." + } +} diff --git a/packages/client/ui-deepseek-model-label-1017/locale/es.json b/packages/client/ui-deepseek-model-label-1017/locale/es.json new file mode 100644 index 0000000000..39db24f093 --- /dev/null +++ b/packages/client/ui-deepseek-model-label-1017/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Insignia del modelo DeepSeek 1017", + "description": "Agregue una insignia de lanzamiento temporal 1017 a los nombres oficiales de los modelos Flash DeepSeek." + } +} diff --git a/packages/client/ui-deepseek-model-label-1017/locale/fr.json b/packages/client/ui-deepseek-model-label-1017/locale/fr.json new file mode 100644 index 0000000000..a0d47622b6 --- /dev/null +++ b/packages/client/ui-deepseek-model-label-1017/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Badge modèle DeepSeek 1017", + "description": "Ajoutez un badge de version 1017 temporaire aux noms de modèles Flash DeepSeek officiels." + } +} diff --git a/packages/client/ui-deepseek-model-label-1017/locale/pt.json b/packages/client/ui-deepseek-model-label-1017/locale/pt.json new file mode 100644 index 0000000000..10be9f4c03 --- /dev/null +++ b/packages/client/ui-deepseek-model-label-1017/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek selo do modelo 1017", + "description": "Adicione um selo de versão temporário 1017 aos nomes oficiais dos modelos do DeepSeek Flash." + } +} diff --git a/packages/client/ui-deepseek-model-label-1017/locale/ru.json b/packages/client/ui-deepseek-model-label-1017/locale/ru.json new file mode 100644 index 0000000000..1d116a03be --- /dev/null +++ b/packages/client/ui-deepseek-model-label-1017/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "DeepSeek Значок модели 1017", + "description": "Добавьте временный значок выпуска 1017 к официальным названиям моделей Flash DeepSeek." + } +} diff --git a/packages/client/ui-deepseek-model-label-1017/src/client/index.ts b/packages/client/ui-deepseek-model-label-1017/src/client/index.ts index 5a38858688..7e35d4e14c 100644 --- a/packages/client/ui-deepseek-model-label-1017/src/client/index.ts +++ b/packages/client/ui-deepseek-model-label-1017/src/client/index.ts @@ -4,7 +4,7 @@ import type {} from '@deepseek-ai/dsh-client-locale/client' import type {} from '@deepseek-ai/dsh-client-ui-model-selection/client' import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import { DeepSeekModelLabel } from './DeepSeekModelLabel.tsx' -import { en, ja, zh, type LabelKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type LabelKey } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { @@ -22,7 +22,7 @@ export const inject = ['slots', 'locale'] */ export function apply(ctx: Context): void { const namespace = 'deepseek-model-label-1017' - ctx.effect(() => ctx.locale.register(namespace, { zh, en, ja }), 'deepseek-model-label-1017: dictionaries') + ctx.effect(() => ctx.locale.register(namespace, { zh, en, ja, es, fr, de, ru, pt }), 'deepseek-model-label-1017: dictionaries') ctx.slots.inject('conversation.input.model.label', () => ctx.slots.register({ name: 'conversation.input.model.label', locale: namespace, }, DeepSeekModelLabel)) diff --git a/packages/client/ui-deepseek-model-label-1017/src/client/locales.ts b/packages/client/ui-deepseek-model-label-1017/src/client/locales.ts index 95ca2cb796..d23df8c829 100644 --- a/packages/client/ui-deepseek-model-label-1017/src/client/locales.ts +++ b/packages/client/ui-deepseek-model-label-1017/src/client/locales.ts @@ -13,3 +13,18 @@ export const en: Record = { 'badge.label': '1017' } export const ja = { 'badge.label': '1017', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { 'badge.label': '1017' } satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { 'badge.label': '1017' } satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { 'badge.label': '1017' } satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { 'badge.label': '1017' } satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { 'badge.label': '1017' } satisfies Record diff --git a/packages/client/ui-deliverables/locale/de.json b/packages/client/ui-deliverables/locale/de.json new file mode 100644 index 0000000000..a87831a167 --- /dev/null +++ b/packages/client/ui-deliverables/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Dokumentänderungen und Lieferpräsentation", + "description": "Anzeige geänderter Dateien, Inhaltsvergleich und Eingabe der Lieferdatei." + } +} diff --git a/packages/client/ui-deliverables/locale/es.json b/packages/client/ui-deliverables/locale/es.json new file mode 100644 index 0000000000..930c363d05 --- /dev/null +++ b/packages/client/ui-deliverables/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Cambios de documentos y presentación de entrega.", + "description": "Muestra archivos modificados, comparación de contenido y entrada de archivos de entrega." + } +} diff --git a/packages/client/ui-deliverables/locale/fr.json b/packages/client/ui-deliverables/locale/fr.json new file mode 100644 index 0000000000..965bcb788d --- /dev/null +++ b/packages/client/ui-deliverables/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Modifications du document et présentation de la livraison", + "description": "Afficher les fichiers modifiés, la comparaison du contenu et la saisie du fichier de livraison." + } +} diff --git a/packages/client/ui-deliverables/locale/pt.json b/packages/client/ui-deliverables/locale/pt.json new file mode 100644 index 0000000000..caef83a2ab --- /dev/null +++ b/packages/client/ui-deliverables/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Alterações de documentos e apresentação de entrega", + "description": "Exibir arquivos modificados, comparação de conteúdo e entrada de arquivo de entrega." + } +} diff --git a/packages/client/ui-deliverables/locale/ru.json b/packages/client/ui-deliverables/locale/ru.json new file mode 100644 index 0000000000..b940a1eba1 --- /dev/null +++ b/packages/client/ui-deliverables/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Изменения документов и представление поставки", + "description": "Отображение измененных файлов, сравнение содержимого и запись файла доставки." + } +} diff --git a/packages/client/ui-deliverables/src/client/index.ts b/packages/client/ui-deliverables/src/client/index.ts index ef5bc4b06d..5750f585f3 100644 --- a/packages/client/ui-deliverables/src/client/index.ts +++ b/packages/client/ui-deliverables/src/client/index.ts @@ -27,7 +27,7 @@ import { DeliverablesTail, type DeliverablesInjected } from './Deliverables.tsx' import { ReviewTab, type ReviewInjected } from './ReviewTab.tsx' import { CHANGES_REVIEW_ID, changesReviewDefinition } from './review-definition.ts' import { createReviewStore } from './review-store.ts' -import { en, ja, NS, zh, type DeliverablesKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type DeliverablesKey } from './locales.ts' import { deliverablesDefinition, presentedForClosing, producedFileMentions, selectProducedFiles, } from './turn-deliverables.ts' @@ -57,7 +57,7 @@ export function apply(ctx: ClientContext): void { diffs.reset() }) ctx.uiConversation.events.register(deliverablesDefinition) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-deliverables: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-deliverables: dictionaries') ctx.slots.inject( 'conversation.chat.turnTail', () => ctx.slots.register({ diff --git a/packages/client/ui-deliverables/src/client/locales.ts b/packages/client/ui-deliverables/src/client/locales.ts index d5c67949ca..e25cc96c43 100644 --- a/packages/client/ui-deliverables/src/client/locales.ts +++ b/packages/client/ui-deliverables/src/client/locales.ts @@ -197,3 +197,323 @@ export const ja = { 'diff.coarse': '行比較がタイムアウトしたため、ファイル全体の置換として表示しています', 'diff.truncated': '最初の{count}行を表示', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'presented.nativeUnavailable': 'Este archivo no tiene una ruta de host disponible. Vista previa en la barra lateral.', + 'presented.revealError': 'No se pudo mostrar en el administrador de archivos. Intentar otra vez.', + 'presented.directoryError': 'No se pudo abrir la carpeta que lo contiene. Intentar otra vez.', + 'presented.directoryOpening': 'Abriendo la carpeta que contiene...', + 'presented.directoryOpened': 'Apertura solicitada que contiene la carpeta', + 'presented.revealed': 'Visualización solicitada en el administrador de archivos.', + 'presented.revealing': 'Mostrando en el administrador de archivos…', + 'presented.unavailable': 'Este Host no tiene un escritorio disponible para abrir archivos o carpetas en aplicaciones externas. Los archivos aún se pueden obtener una vista previa en la barra lateral.', + 'presented.retry': 'Reintentar', + 'presented.hostError': 'No se pudo leer la información del escritorio del host', + 'presented.preview': 'Vista previa en la barra lateral', + 'presented.previewButton': 'Abra {name} en la barra lateral', + 'presented.previewCard': 'Vista previa de {name} en la barra lateral', + 'presented.all': 'Todos los archivos {count}', + 'presented.expandAria': 'Mostrar todos los archivos entregados {count}', + 'presented.collapse': 'Contraer', + 'presented.collapseAria': 'Contraer archivos entregados', + 'presented.opening': 'Abriendo…', + 'presented.opened': 'Abierto solicitado', + 'presented.error': 'No se pudo abrir. Haga clic para volver a intentarlo.', + 'presented.file': 'Archivo', + 'row.title': 'Presentar archivos', + 'row.running': 'Entregando', + 'row.preparing': 'Preparando entregables', + 'row.ok': 'Entregado', + 'row.error': 'Entrega fallida', + 'row.stopped': 'interrumpido', + 'row.inspect': 'inspeccionar llamada', + 'changes.title': 'Archivos {count} editados', + 'changes.singleTitle': 'Editado {name}', + 'changes.added': '+{count}', + 'changes.deleted': '-{count}', + 'changes.binary': 'binario', + 'changes.openReview': 'Revisa los cambios de este turno en la barra lateral.', + 'changes.all': 'Todos los archivos {count}', + 'changes.expandAria': 'Mostrar todos los archivos modificados {count}', + 'changes.collapse': 'Contraer', + 'changes.collapseAria': 'Contraer archivos modificados', + 'changes.oversized': 'demasiado grande', + 'changes.viewDiff': 'Ver cambios en {name}', + 'review.title': 'Revisión · turno {turn}', + 'review.selectFile': 'Elija el archivo para revisar', + 'review.split': 'Cambiar a vista dividida', + 'review.unified': 'Cambiar a vista unificada', + 'review.splitAria': 'Vista dividida', + 'review.wrap': 'Habilitar ajuste de línea', + 'review.nowrap': 'Desactivar ajuste de línea', + 'review.wrapAria': 'Ajuste de línea', + 'review.openFile': 'Abra el archivo completo en la barra lateral.', + 'review.openFileAria': 'Abra {name} en la barra lateral', + 'diff.loading': 'Cambios de lectura…', + 'diff.missing': 'El contenido de los cambios de este turno ya no está disponible.', + 'diff.error': 'No se pudieron leer los cambios.', + 'diff.binary': 'Archivo binario; los cambios no se pueden mostrar', + 'diff.oversized': 'Archivo demasiado grande; los cambios no se pueden mostrar', + 'diff.created': 'Creado en este turno', + 'diff.deleted': 'Eliminado en este turno', + 'diff.unchanged': 'Ambos lados mantienen las mismas líneas.', + 'diff.coarse': 'Se agotó el tiempo de espera para la comparación de líneas; se muestra como un reemplazo de archivo completo', + 'diff.truncated': 'Mostrando las primeras líneas {count}', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'presented.nativeUnavailable': "Ce fichier n'a pas de chemin d'hôte disponible. Prévisualisez-le dans la barre latérale.", + 'presented.revealError': "Impossible d'afficher dans le gestionnaire de fichiers. Essayer à nouveau.", + 'presented.directoryError': "Impossible d'ouvrir le dossier contenant. Essayer à nouveau.", + 'presented.directoryOpening': 'Ouverture du dossier contenant…', + 'presented.directoryOpened': 'Ouverture demandée contenant le dossier', + 'presented.revealed': 'Affichage demandé dans le gestionnaire de fichiers', + 'presented.revealing': 'Affichage dans le gestionnaire de fichiers…', + 'presented.unavailable': 'Cet hôte ne dispose pas de bureau disponible pour ouvrir des fichiers ou des dossiers dans des applications externes. Les fichiers peuvent toujours être prévisualisés dans la barre latérale.', + 'presented.retry': 'Réessayer', + 'presented.hostError': 'Impossible de lire les informations du bureau hôte', + 'presented.preview': 'Aperçu dans la barre latérale', + 'presented.previewButton': 'Ouvrir {name} dans la barre latérale', + 'presented.previewCard': 'Aperçu de {name} dans la barre latérale', + 'presented.all': 'Tous les fichiers {count}', + 'presented.expandAria': 'Afficher tous les fichiers livrés {count}', + 'presented.collapse': 'Réduire', + 'presented.collapseAria': 'Réduire les fichiers livrés', + 'presented.opening': 'Ouverture…', + 'presented.opened': 'Ouvert demandé', + 'presented.error': "Impossible d'ouvrir. Cliquez pour réessayer.", + 'presented.file': 'Fichier', + 'row.title': 'Présenter des fichiers', + 'row.running': 'Livrer', + 'row.preparing': 'Préparation des livrables', + 'row.ok': 'Livré', + 'row.error': 'La livraison a échoué', + 'row.stopped': 'Interrompu', + 'row.inspect': "Inspecter l'appel", + 'changes.title': 'Fichiers {count} modifiés', + 'changes.singleTitle': 'Modifié {name}', + 'changes.added': '+{count}', + 'changes.deleted': '-{count}', + 'changes.binary': 'binaire', + 'changes.openReview': 'Consultez les modifications de ce tour dans la barre latérale', + 'changes.all': 'Tous les fichiers {count}', + 'changes.expandAria': 'Afficher tous les fichiers modifiés {count}', + 'changes.collapse': 'Réduire', + 'changes.collapseAria': 'Réduire les fichiers modifiés', + 'changes.oversized': 'trop grand', + 'changes.viewDiff': 'Afficher les modifications apportées à {name}', + 'review.title': 'Révision · tour {turn}', + 'review.selectFile': 'Choisissez le fichier à examiner', + 'review.split': 'Passer à la vue fractionnée', + 'review.unified': 'Passer à la vue unifiée', + 'review.splitAria': 'Vue fractionnée', + 'review.wrap': 'Activer le retour à la ligne', + 'review.nowrap': 'Désactiver le retour à la ligne', + 'review.wrapAria': 'Retour à la ligne', + 'review.openFile': "Ouvrez l'intégralité du fichier dans la barre latérale", + 'review.openFileAria': 'Ouvrir {name} dans la barre latérale', + 'diff.loading': 'La lecture change…', + 'diff.missing': "Le contenu des modifications de ce tour n'est plus disponible", + 'diff.error': 'Impossible de lire les modifications', + 'diff.binary': 'Fichier binaire ; les modifications ne peuvent pas être affichées', + 'diff.oversized': 'Fichier trop volumineux ; les modifications ne peuvent pas être affichées', + 'diff.created': 'Créé à ce tour', + 'diff.deleted': 'Supprimé à ce tour', + 'diff.unchanged': 'Les deux côtés tiennent les mêmes lignes', + 'diff.coarse': "La comparaison de lignes a expiré ; affiché en remplacement de l'ensemble du fichier", + 'diff.truncated': 'Affichage des premières lignes {count}', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'presented.nativeUnavailable': 'Für diese Datei ist kein Hostpfad verfügbar. Sehen Sie sich die Vorschau in der Seitenleiste an.', + 'presented.revealError': 'Konnte im Dateimanager nicht angezeigt werden. Versuchen Sie es erneut.', + 'presented.directoryError': 'Der enthaltende Ordner konnte nicht geöffnet werden. Versuchen Sie es erneut.', + 'presented.directoryOpening': 'Öffnen des enthaltenden Ordners…', + 'presented.directoryOpened': 'Angeforderte Öffnung mit Ordner', + 'presented.revealed': 'Angeforderte Anzeige im Dateimanager', + 'presented.revealing': 'Wird im Dateimanager angezeigt…', + 'presented.unavailable': 'Auf diesem Host steht kein Desktop zum Öffnen von Dateien oder Ordnern in externen Apps zur Verfügung. Dateien können weiterhin in der Seitenleiste in der Vorschau angezeigt werden.', + 'presented.retry': 'Erneut versuchen', + 'presented.hostError': 'Die Host-Desktop-Informationen konnten nicht gelesen werden', + 'presented.preview': 'Vorschau in der Seitenleiste', + 'presented.previewButton': 'Öffnen Sie {name} in der Seitenleiste', + 'presented.previewCard': 'Vorschau von {name} in der Seitenleiste', + 'presented.all': 'Alle {count}-Dateien', + 'presented.expandAria': 'Alle von {count} gelieferten Dateien anzeigen', + 'presented.collapse': 'Einklappen', + 'presented.collapseAria': 'Gelieferte Dateien ausblenden', + 'presented.opening': 'Eröffnung…', + 'presented.opened': 'Öffnung erbeten', + 'presented.error': 'Konnte nicht geöffnet werden. Klicken Sie, um es erneut zu versuchen.', + 'presented.file': 'Datei', + 'row.title': 'Vorhandene Akten', + 'row.running': 'Liefern', + 'row.preparing': 'Vorbereiten von Liefergegenständen', + 'row.ok': 'Geliefert', + 'row.error': 'Die Lieferung ist fehlgeschlagen', + 'row.stopped': 'Unterbrochen', + 'row.inspect': 'Aufruf prüfen', + 'changes.title': 'Bearbeitete {count}-Dateien', + 'changes.singleTitle': 'Bearbeitet {name}', + 'changes.added': '+{count}', + 'changes.deleted': '-{count}', + 'changes.binary': 'binär', + 'changes.openReview': 'Überprüfen Sie die Änderungen dieser Runde in der Seitenleiste', + 'changes.all': 'Alle {count}-Dateien', + 'changes.expandAria': 'Alle von {count} geänderten Dateien anzeigen', + 'changes.collapse': 'Einklappen', + 'changes.collapseAria': 'Geänderte Dateien reduzieren', + 'changes.oversized': 'zu groß', + 'changes.viewDiff': 'Änderungen an {name} anzeigen', + 'review.title': 'Überprüfung · Runde {turn}', + 'review.selectFile': 'Wählen Sie die zu überprüfende Datei aus', + 'review.split': 'Wechseln Sie zur geteilten Ansicht', + 'review.unified': 'Wechseln Sie zur einheitlichen Ansicht', + 'review.splitAria': 'Geteilte Ansicht', + 'review.wrap': 'Zeilenumbruch aktivieren', + 'review.nowrap': 'Zeilenumbruch deaktivieren', + 'review.wrapAria': 'Zeilenumbruch', + 'review.openFile': 'Öffnen Sie die gesamte Datei in der Seitenleiste', + 'review.openFileAria': 'Öffnen Sie {name} in der Seitenleiste', + 'diff.loading': 'Änderungen lesen…', + 'diff.missing': 'Der Inhalt der Änderungen dieser Runde ist nicht mehr verfügbar', + 'diff.error': 'Die Änderungen konnten nicht gelesen werden', + 'diff.binary': 'Binärdatei; Änderungen können nicht angezeigt werden', + 'diff.oversized': 'Datei zu groß; Änderungen können nicht angezeigt werden', + 'diff.created': 'In dieser Runde erstellt', + 'diff.deleted': 'In dieser Runde gelöscht', + 'diff.unchanged': 'Beide Seiten vertreten die gleichen Linien', + 'diff.coarse': 'Zeitüberschreitung beim Leitungsvergleich; wird als vollständige Dateiersetzung angezeigt', + 'diff.truncated': 'Zeigt die ersten {count}-Zeilen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'presented.nativeUnavailable': 'У этого файла нет доступного пути к хосту. Предварительный просмотр на боковой панели.', + 'presented.revealError': 'Не удалось отобразить в файловом менеджере. Попробуйте еще раз.', + 'presented.directoryError': 'Не удалось открыть содержащую папку. Попробуйте еще раз.', + 'presented.directoryOpening': 'Открытие папки, содержащей…', + 'presented.directoryOpened': 'Запрошенное открытие, содержащее папку', + 'presented.revealed': 'Запрошенное отображение в файловом менеджере', + 'presented.revealing': 'Отображается в файловом менеджере…', + 'presented.unavailable': 'У этого хоста нет рабочего стола, на котором можно было бы открывать файлы или папки во внешних приложениях. Файлы по-прежнему можно просматривать на боковой панели.', + 'presented.retry': 'Повторить', + 'presented.hostError': 'Не удалось прочитать информацию о рабочем столе хоста.', + 'presented.preview': 'Предварительный просмотр в боковой панели', + 'presented.previewButton': 'Откройте {name} на боковой панели.', + 'presented.previewCard': 'Предварительный просмотр {name} на боковой панели', + 'presented.all': 'Все файлы {count}', + 'presented.expandAria': 'Показать все доставленные файлы {count}', + 'presented.collapse': 'Свернуть', + 'presented.collapseAria': 'Свернуть доставленные файлы', + 'presented.opening': 'Открытие…', + 'presented.opened': 'Открыть запрос', + 'presented.error': 'Не удалось открыть. Нажмите, чтобы повторить попытку.', + 'presented.file': 'Файл', + 'row.title': 'Представление файлов', + 'row.running': 'Доставка', + 'row.preparing': 'Подготовка результатов', + 'row.ok': 'Доставлено', + 'row.error': 'Доставка не удалась', + 'row.stopped': 'Прерванный', + 'row.inspect': 'Проверить вызов', + 'changes.title': 'Отредактированные файлы {count}.', + 'changes.singleTitle': 'Отредактировано {name}', + 'changes.added': '+{count}', + 'changes.deleted': '-{count}', + 'changes.binary': 'двоичный', + 'changes.openReview': 'Просмотрите изменения этого хода на боковой панели.', + 'changes.all': 'Все файлы {count}', + 'changes.expandAria': 'Показать все измененные файлы {count}', + 'changes.collapse': 'Свернуть', + 'changes.collapseAria': 'Свернуть измененные файлы', + 'changes.oversized': 'слишком большой', + 'changes.viewDiff': 'Посмотреть изменения в {name}', + 'review.title': 'Проверка · ход {turn}', + 'review.selectFile': 'Выберите файл для просмотра', + 'review.split': 'Переключиться на разделенный вид', + 'review.unified': 'Переключиться на единый вид', + 'review.splitAria': 'Разделенный вид', + 'review.wrap': 'Включить перенос строк', + 'review.nowrap': 'Отключить перенос строк', + 'review.wrapAria': 'Перенос строки', + 'review.openFile': 'Открыть весь файл на боковой панели', + 'review.openFileAria': 'Откройте {name} на боковой панели.', + 'diff.loading': 'Чтение изменений…', + 'diff.missing': 'Содержимое изменений этого хода больше не доступно.', + 'diff.error': 'Не удалось прочитать изменения', + 'diff.binary': 'Бинарный файл; изменения не могут быть показаны', + 'diff.oversized': 'Файл слишком большой; изменения не могут быть показаны', + 'diff.created': 'Создано в этом ходу', + 'diff.deleted': 'Удален на этом ходу', + 'diff.unchanged': 'Обе стороны держат одни и те же линии', + 'diff.coarse': 'Время сравнения строк истекло; отображается как замена всего файла', + 'diff.truncated': 'Показаны первые строки {count}', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'presented.nativeUnavailable': 'Este arquivo não possui caminho de host disponível. Visualize-o na barra lateral.', + 'presented.revealError': 'Não foi possível mostrar no gerenciador de arquivos. Tente novamente.', + 'presented.directoryError': 'Não foi possível abrir a pasta que contém. Tente novamente.', + 'presented.directoryOpening': 'Abrindo a pasta que contém…', + 'presented.directoryOpened': 'Abertura solicitada contendo pasta', + 'presented.revealed': 'Exibição solicitada no gerenciador de arquivos', + 'presented.revealing': 'Mostrando no gerenciador de arquivos…', + 'presented.unavailable': 'Este Host não possui área de trabalho disponível para abrir arquivos ou pastas em aplicativos externos. Os arquivos ainda podem ser visualizados na barra lateral.', + 'presented.retry': 'Tentar novamente', + 'presented.hostError': 'Não foi possível ler as informações da área de trabalho do Host', + 'presented.preview': 'Pré-visualização na barra lateral', + 'presented.previewButton': 'Abra {name} na barra lateral', + 'presented.previewCard': 'Visualizar {name} na barra lateral', + 'presented.all': 'Todos os {count} arquivos', + 'presented.expandAria': 'Mostrar todos os {count} arquivos entregues', + 'presented.collapse': 'Recolher', + 'presented.collapseAria': 'Recolher arquivos entregues', + 'presented.opening': 'Abrindo…', + 'presented.opened': 'Aberto solicitado', + 'presented.error': 'Não foi possível abrir. Clique para tentar novamente.', + 'presented.file': 'Arquivo', + 'row.title': 'Apresentar arquivos', + 'row.running': 'Entregando', + 'row.preparing': 'Preparando resultados', + 'row.ok': 'Entregue', + 'row.error': 'Falha na entrega', + 'row.stopped': 'Interrompido', + 'row.inspect': 'Inspecionar chamada', + 'changes.title': '{count} arquivos editados', + 'changes.singleTitle': 'Editado {name}', + 'changes.added': '+{count}', + 'changes.deleted': '-{count}', + 'changes.binary': 'binário', + 'changes.openReview': 'Revise as mudanças deste turno na barra lateral', + 'changes.all': 'Todos os {count} arquivos', + 'changes.expandAria': 'Mostrar todos os {count} arquivos alterados', + 'changes.collapse': 'Recolher', + 'changes.collapseAria': 'Recolher arquivos alterados', + 'changes.oversized': 'muito grande', + 'changes.viewDiff': 'Ver alterações em {name}', + 'review.title': 'Revisão · turno {turn}', + 'review.selectFile': 'Escolha o arquivo para revisar', + 'review.split': 'Mudar para visualização dividida', + 'review.unified': 'Mudar para visualização unificada', + 'review.splitAria': 'Visualização dividida', + 'review.wrap': 'Ativar quebra de linha', + 'review.nowrap': 'Desativar quebra de linha', + 'review.wrapAria': 'Quebra de linha', + 'review.openFile': 'Abra o arquivo inteiro na barra lateral', + 'review.openFileAria': 'Abra {name} na barra lateral', + 'diff.loading': 'A leitura muda…', + 'diff.missing': 'O conteúdo das alterações deste turno não está mais disponível', + 'diff.error': 'Não foi possível ler as alterações', + 'diff.binary': 'Arquivo binário; as alterações não podem ser mostradas', + 'diff.oversized': 'Arquivo muito grande; as alterações não podem ser mostradas', + 'diff.created': 'Criado neste turno', + 'diff.deleted': 'Excluído neste turno', + 'diff.unchanged': 'Ambos os lados mantêm as mesmas linhas', + 'diff.coarse': 'A comparação de linha expirou; mostrado como uma substituição de arquivo inteiro', + 'diff.truncated': 'Mostrando as primeiras {count} linhas', +} satisfies Record diff --git a/packages/client/ui-directory-picker-browse/locale/de.json b/packages/client/ui-directory-picker-browse/locale/de.json new file mode 100644 index 0000000000..0ae1c16fad --- /dev/null +++ b/packages/client/ui-directory-picker-browse/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zur In-App-Verzeichnisauswahl", + "description": "Durchsuchen, erstellen und wählen Sie ein Arbeitsbereichsverzeichnis innerhalb der App aus." + } +} diff --git a/packages/client/ui-directory-picker-browse/locale/es.json b/packages/client/ui-directory-picker-browse/locale/es.json new file mode 100644 index 0000000000..1ae27d3528 --- /dev/null +++ b/packages/client/ui-directory-picker-browse/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de selección de directorio en la aplicación", + "description": "Busque, cree y seleccione un directorio de espacio de trabajo dentro de la aplicación." + } +} diff --git a/packages/client/ui-directory-picker-browse/locale/fr.json b/packages/client/ui-directory-picker-browse/locale/fr.json new file mode 100644 index 0000000000..c68b6dfdcd --- /dev/null +++ b/packages/client/ui-directory-picker-browse/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de sélection de répertoire dans l'application", + "description": "Parcourez, créez et sélectionnez un répertoire d'espace de travail dans l'application." + } +} diff --git a/packages/client/ui-directory-picker-browse/locale/pt.json b/packages/client/ui-directory-picker-browse/locale/pt.json new file mode 100644 index 0000000000..236a105e59 --- /dev/null +++ b/packages/client/ui-directory-picker-browse/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de seleção de diretório no aplicativo", + "description": "Navegue, crie e selecione um diretório de espaço de trabalho no aplicativo." + } +} diff --git a/packages/client/ui-directory-picker-browse/locale/ru.json b/packages/client/ui-directory-picker-browse/locale/ru.json new file mode 100644 index 0000000000..5c2a88cff4 --- /dev/null +++ b/packages/client/ui-directory-picker-browse/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс выбора каталога в приложении", + "description": "Просмотрите, создайте и выберите каталог рабочей области в приложении." + } +} diff --git a/packages/client/ui-directory-picker-browse/src/client/index.ts b/packages/client/ui-directory-picker-browse/src/client/index.ts index bc78900e0b..d3cff16b31 100644 --- a/packages/client/ui-directory-picker-browse/src/client/index.ts +++ b/packages/client/ui-directory-picker-browse/src/client/index.ts @@ -80,6 +80,81 @@ export function apply(ctx: ClientContext): void { 'browser.truncated': 'フォルダが多すぎて一覧表示できません。先頭のみ表示しています。', 'browser.showHidden': '隠しファイルを表示', }], + ['es', { + 'browser.title': 'Seleccionar carpeta de trabajo', + 'browser.home': 'Inicio', + 'browser.newFolder': 'Nueva carpeta', + 'browser.folderName': 'Nombre de la carpeta', + 'browser.createIn': 'Nueva carpeta en "{name}"', + 'browser.untitledFolder': 'Carpeta sin título', + 'browser.create': 'Crear', + 'browser.cancel': 'Cancelar', + 'browser.open': 'Abrir', + 'browser.editPath': 'Editar ruta', + 'browser.loading': 'Cargando…', + 'browser.truncated': 'Hay demasiadas carpetas; solo se muestra el principio.', + 'browser.showHidden': 'Mostrar archivos ocultos', + }], + ['fr', { + 'browser.title': 'Choisir le dossier de travail', + 'browser.home': 'Dossier personnel', + 'browser.newFolder': 'Nouveau dossier', + 'browser.folderName': 'Nom du dossier', + 'browser.createIn': 'Nouveau dossier dans « {name} »', + 'browser.untitledFolder': 'Dossier sans titre', + 'browser.create': 'Créer', + 'browser.cancel': 'Annuler', + 'browser.open': 'Ouvrir', + 'browser.editPath': 'Modifier le chemin', + 'browser.loading': 'Chargement…', + 'browser.truncated': 'Trop de dossiers à afficher ; seul le début est affiché.', + 'browser.showHidden': 'Afficher les fichiers cachés', + }], + ['de', { + 'browser.title': 'Arbeitsverzeichnis auswählen', + 'browser.home': 'Persönlicher Ordner', + 'browser.newFolder': 'Neuer Ordner', + 'browser.folderName': 'Ordnername', + 'browser.createIn': 'Neuer Ordner in „{name}“', + 'browser.untitledFolder': 'Unbenannter Ordner', + 'browser.create': 'Erstellen', + 'browser.cancel': 'Abbrechen', + 'browser.open': 'Öffnen', + 'browser.editPath': 'Pfad bearbeiten', + 'browser.loading': 'Laden…', + 'browser.truncated': 'Zu viele Ordner; nur der Anfang wird angezeigt.', + 'browser.showHidden': 'Versteckte Dateien anzeigen', + }], + ['ru', { + 'browser.title': 'Выбрать рабочую папку', + 'browser.home': 'Домашняя папка', + 'browser.newFolder': 'Новая папка', + 'browser.folderName': 'Имя папки', + 'browser.createIn': 'Новая папка в «{name}»', + 'browser.untitledFolder': 'Папка без названия', + 'browser.create': 'Создать', + 'browser.cancel': 'Отмена', + 'browser.open': 'Открыть', + 'browser.editPath': 'Изменить путь', + 'browser.loading': 'Загрузка…', + 'browser.truncated': 'Папок слишком много; показана только начальная часть списка.', + 'browser.showHidden': 'Показать скрытые файлы', + }], + ['pt', { + 'browser.title': 'Selecionar pasta de trabalho', + 'browser.home': 'Pasta pessoal', + 'browser.newFolder': 'Nova pasta', + 'browser.folderName': 'Nome da pasta', + 'browser.createIn': 'Nova pasta em "{name}"', + 'browser.untitledFolder': 'Pasta sem nome', + 'browser.create': 'Criar', + 'browser.cancel': 'Cancelar', + 'browser.open': 'Abrir', + 'browser.editPath': 'Editar caminho', + 'browser.loading': 'Carregando…', + 'browser.truncated': 'Há pastas demais; apenas o início é exibido.', + 'browser.showHidden': 'Mostrar arquivos ocultos', + }], ] try { for (const [locale, dict] of dictionaries) disposers.push(ctx.locale.register(LOCALE_NS, locale, dict)) diff --git a/packages/client/ui-directory-picker-native/locale/de.json b/packages/client/ui-directory-picker-native/locale/de.json new file mode 100644 index 0000000000..d0bd0ff6aa --- /dev/null +++ b/packages/client/ui-directory-picker-native/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zur Auswahl des Systemverzeichnisses", + "description": "Verbinden Sie das Arbeitsbereich-Auswahlportal und das Systemverzeichnis-Auswahldialogfeld." + } +} diff --git a/packages/client/ui-directory-picker-native/locale/es.json b/packages/client/ui-directory-picker-native/locale/es.json new file mode 100644 index 0000000000..67dfd34b96 --- /dev/null +++ b/packages/client/ui-directory-picker-native/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de selección del directorio del sistema", + "description": "Conecte el portal de selección del espacio de trabajo y el cuadro de diálogo de selección del directorio del sistema." + } +} diff --git a/packages/client/ui-directory-picker-native/locale/fr.json b/packages/client/ui-directory-picker-native/locale/fr.json new file mode 100644 index 0000000000..87bff91413 --- /dev/null +++ b/packages/client/ui-directory-picker-native/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de sélection du répertoire système", + "description": "Connectez le portail de sélection de l'espace de travail et la boîte de dialogue de sélection du répertoire système." + } +} diff --git a/packages/client/ui-directory-picker-native/locale/pt.json b/packages/client/ui-directory-picker-native/locale/pt.json new file mode 100644 index 0000000000..908a8e4105 --- /dev/null +++ b/packages/client/ui-directory-picker-native/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de seleção de diretório do sistema", + "description": "Conecte o portal de seleção de espaço de trabalho e a caixa de diálogo de seleção de diretório do sistema." + } +} diff --git a/packages/client/ui-directory-picker-native/locale/ru.json b/packages/client/ui-directory-picker-native/locale/ru.json new file mode 100644 index 0000000000..e57e07e0d0 --- /dev/null +++ b/packages/client/ui-directory-picker-native/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс выбора системного каталога", + "description": "Соедините портал выбора рабочего пространства и диалоговое окно выбора системного каталога." + } +} diff --git a/packages/client/ui-entry-customization/README.i18n.yaml b/packages/client/ui-entry-customization/README.i18n.yaml index 021b70ca20..acd7ff8b16 100644 --- a/packages/client/ui-entry-customization/README.i18n.yaml +++ b/packages/client/ui-entry-customization/README.i18n.yaml @@ -3,23 +3,23 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-entry-customization/README.md /: - en: 200adf0dd8c9757c - zh: 3d10d49a71289702 + en: 57416280d387ea2b + zh: f9b97e610116bf59 /deepseek-ai-dsh-client-ui-entry-customization: en: ab35194f4a9dafce zh: 7e9e57fde518c3d7 /deepseek-ai-dsh-client-ui-entry-customization/summary: - en: d53e7d71da9fd4d6 - zh: a32b635ec1e572f3 + en: 589199344fe3dd70 + zh: e5e3003c5c4d9479 /deepseek-ai-dsh-client-ui-entry-customization/table-of-contents: en: 76368b6b1da79c80 zh: 52384aee991ac542 /deepseek-ai-dsh-client-ui-entry-customization/use-this-package: - en: e642513bcfd42d68 - zh: 38b9109ec017900d + en: 795f14554e3ff1b0 + zh: ed6d8702c9cfa4e7 /deepseek-ai-dsh-client-ui-entry-customization/understand-the-implementation: - en: 2f1a873bebc098c8 - zh: c2ec33e50ff2ccc0 + en: 65738fbe4d77e47a + zh: 201acc369942cab4 /deepseek-ai-dsh-client-ui-entry-customization/model-experience: en: b891f311c09d536f zh: 68c0f41fbfed0d73 @@ -27,8 +27,8 @@ en: dc50ebf94183efa2 zh: 617803c558367430 /deepseek-ai-dsh-client-ui-entry-customization/known-limitations-and-deferred-work: - en: 2e582c76ffe9d663 - zh: 7e5e49ad3d6f0f83 + en: 5b4914a3ef05fa9c + zh: 9cbe8a17a88ee4d6 /deepseek-ai-dsh-client-ui-entry-customization/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/client/ui-entry-customization/README.md b/packages/client/ui-entry-customization/README.md index c05efa4379..148fd6862d 100644 --- a/packages/client/ui-entry-customization/README.md +++ b/packages/client/ui-entry-customization/README.md @@ -1,5 +1,5 @@ --- -description: "Fill the More menus of three navigation regions with pin controls, plugin browsing, and a Creator draft." +description: "Offer plugin browsing and a Creator draft in three navigation regions." kind: "package-reference" --- @@ -11,7 +11,7 @@ Desktop product events use the optional [product analytics service](../product-a ## Summary -Fill the More menus of the left sidebar, Conversation View tabs, and right-sidebar guide with each region's customizable entries, pin buttons, plugin browsing, and a reviewable Creator draft. The Web app loads it by default. Each region draws its own More button and stores its own pin choices; this package stores nothing. +Browse plugins and prepare a reviewable Creator draft from the More menus of the left sidebar, Conversation View tabs, and right-sidebar guide. The Web app loads this package by default. Each region draws its own More button; this package stores nothing. ## Table of Contents @@ -28,13 +28,13 @@ Fill the More menus of the left sidebar, Conversation View tabs, and right-sideb The `@deepseek-ai/dsh-web-app` bundle loads this package by default; it needs no Plugins switch or profile selection. A region shows its More button only while its actions slot has a registration, so unloading this package removes all three buttons. -Open **More** beneath the left navigation entries, **More conversation views** after the Conversation View tabs, or **More tools** after the right-sidebar guide cards. Each menu lists the region's customizable entries in registration order; Plugins, Chat, and Workspace files always show and are not listed. Selecting an entry's name closes the menu and opens the entry without changing its pin state. The separate pin button pins or unpins the region's launcher and keeps the menu open; a filled pin marks a pinned entry. Unpinned entries stay reachable through the menu, and pin choices persist across reloads. Long entry names wrap, except in the More tools menu, where they truncate to one line. +Open **More** beneath the left navigation entries, **More conversation views** after the Conversation View tabs, or **More tools** after the right-sidebar guide cards. Left-sidebar and guide menus offer only **Browse other plugins** (when available) and **Create your plugin**. The Conversation menu also lists folded Views before those actions and marks the active View with a check on the right. None of these menus provide pin controls or sorting. -**Browse other plugins** opens the Plugins main list without opening an installation dialog. **Create your plugin** opens a new or reusable blank Session with the Creator preset and adds a short localized request for a plugin in that region to its draft: an empty draft takes the request as its text, and an existing draft keeps its text, references, and attachments with the request appended as a new paragraph. It never sends a message. Review and send the draft yourself. Both actions remain available when a region has no customizable entries; an empty list adds no separator. +**Browse other plugins** opens the Plugins main list without opening an installation dialog. **Create your plugin** opens a new or reusable blank Session with the Creator preset and adds a short localized request for a plugin in that region to its draft: an empty draft takes the request as its text, and an existing draft keeps its text, references, and attachments with the request appended as a new paragraph. It never sends a message. Review and send the draft yourself. Creation remains available regardless of the region’s entries; a separator appears only between folded Views and the actions, before browsing when available or before creation otherwise. -On Desktop, **Browse other plugins** and **Create your plugin** report `more_menu_item_click` before they act, with `action=browse_plugins` or `action=create_plugin` and the menu's `region`: `sidebar` for **More**, `conversation` for **More conversation views**, and `rightbar` for **More tools**. Opening an entry and changing a pin report nothing, and the menus report no exposure events. Without the analytics service, both actions behave the same and report nothing. +On Desktop, **Browse other plugins** and **Create your plugin** report `more_menu_item_click` before they act, with `action=browse_plugins` or `action=create_plugin` and the menu's `region`: `sidebar` for **More**, `conversation` for **More conversation views**, and `rightbar` for **More tools**. The menus report no exposure events. Without the analytics service, both actions behave the same and report nothing. -The browse-plugins row appears only while the `pluginNavigation` service is present; the plugin starts without it. +The browse-plugins row appears only while the `pluginNavigation` service is present; the plugin starts without it. Its absence removes only browsing, not More buttons, folded Views, or creation. ----- @@ -44,7 +44,7 @@ The browse-plugins row appears only while the `pluginNavigation` service is pres
Implementation internals — click to expand -[`apply`](src/client/index.ts) registers [`EntryMenuContent`](src/client/EntryMenuContent.tsx) under id `entry-customization` into `sidebar.panellist.actions`, `conversation.session.header.views.actions`, and `sidebar.right.tab.guide.actions` through `ctx.slots.inject`, so each registration follows its region's declaration lifetime. The regions render these slots inside their own `Menu` and supply the entries, their pin state, `onOpen`, `onPinnedChange`, and `onDismiss` as owner props. The rows call `onDismiss` before opening an entry or running a footer action; pin changes leave the menu open. +[`apply`](src/client/index.ts) registers [`EntryMenuContent`](src/client/EntryMenuContent.tsx) under id `entry-customization` into `sidebar.panellist.actions`, `conversation.session.header.views.actions`, and `sidebar.right.tab.guide.actions` through `ctx.slots.inject`, so each registration follows its region's declaration lifetime. The regions render these slots inside their own `Menu`; the rows call `onDismiss` before browsing or creating. Only the Conversation menu consumes its owner’s folded entry list and navigation callback; pin callbacks remain unused. Each registration's `inject` supplies its region and two callbacks. Each callback first calls `ctx.get('productAnalytics')?.track('more_menu_item_click', { region, action })`. Browsing then calls `ctx.get('pluginNavigation')?.openList()`; ui-plugin-manager provides that service only while the Plugins page has access, so this package reads it optionally and never lists it in `inject`. Creation then calls `uiWorkspace.startSession(undefined, { agentPreset: 'cordis', prompt, whenDraftExists: 'append' })` with the region's localized prompt; it does not go through `ui-agent-preset`, so the preset chip plays no introduction. The [`dsh-web-app` bundle patch](../../bundle/web-app/cordis.patch.yml) mounts the browser plugin. @@ -65,12 +65,12 @@ None; the package neither assembles nor sends model requests. -- If this package is disabled or fails to load after entries were unpinned, those launchers stay hidden and no More menu can restore them. Re-enable the package or clear the region's `dsh.slot.manual-order.*.v1` localStorage key. -- Pin choices live in browser localStorage, which is isolated by protocol, host, and port: changing the Web port or switching between Web and Desktop does not carry them over, and they do not sync across devices. +- More menus provide no visibility controls. Region pin callbacks remain available to other registrations and change only in-memory choices; plugin reload restores defaults. +- Legacy pin values in localStorage are neither read nor written. - **Create your plugin** does not check whether the Creator preset exists. Without it, Session creation fails, the Workspace shows its creation-failure notice, and no draft is prepared. Draft preparation also requires a resolvable Workspace; without one, the existing flow reports that a Workspace is required. -- Entries keep registration order; the menus offer no reordering, and entries cannot move between regions. +- Conversation owns the estimated tab split; these menus provide no sorting. - Conversation controls follow the existing nonblank-Session header and Coding Tools eligibility. Right-sidebar controls require a selected Session and appear on its guide page. -- Only active registered launchers appear; the menus are not an inventory of disabled or uninstalled plugins. +- The menus do not list plugins; use the browsing action to open the Plugins page. ### Dev Note diff --git a/packages/client/ui-entry-customization/README.zh.md b/packages/client/ui-entry-customization/README.zh.md index daa91719b3..4bb7cf5379 100644 --- a/packages/client/ui-entry-customization/README.zh.md +++ b/packages/client/ui-entry-customization/README.zh.md @@ -1,5 +1,5 @@ --- -description: "为三个导航区域的“更多”菜单提供 Pin 控件、插件浏览与创造模式草稿。" +description: "为三个导航区域的更多菜单提供插件浏览与创造模式草稿。" kind: "package-reference" --- @@ -11,7 +11,7 @@ kind: "package-reference" ## 概述 -为左侧栏、会话 View 标签和右侧栏起始页的“更多”菜单提供内容:各区域可自定义的入口、Pin 按钮、插件浏览与可审阅的创造模式草稿。Web 应用默认加载本包。各区域自己画“更多”按钮并保存自己的 Pin 状态;本包不保存状态。 +为左侧栏、会话 View 标签和右侧栏起始页的“更多”菜单提供插件浏览与可审阅的创造模式草稿。Web 应用默认加载本包。各区域自己画“更多”按钮;本包不保存状态。 ## 目录 @@ -28,13 +28,13 @@ kind: "package-reference" `@deepseek-ai/dsh-web-app` 组合包默认加载本包,无需在插件页开启或由 profile 选择。区域只在其 actions slot 有注册项时显示“更多”按钮,因此卸载本包会移除三个按钮。 -打开左侧导航入口下方的**更多**、会话 View 标签之后的**更多会话视图**,或右侧栏起始页卡片之后的**更多工具**。每个菜单按注册顺序列出该区域可自定义的入口;插件管理、Chat 和工作区文件始终显示,不列入菜单。点击入口名称会关闭菜单并打开该入口,不改变 Pin 状态。独立的 Pin 按钮在区域中固定或收起该入口,并保持菜单打开;实心 Pin 表示已固定。已收起的入口仍可从菜单打开,Pin 状态在刷新后保留。过长的入口名称会换行,更多工具菜单中则截断为单行。 +打开左侧导航入口下方的**更多**、会话 View 标签之后的**更多会话视图**,或右侧栏起始页卡片之后的**更多工具**。左侧栏和起始页菜单仅提供**浏览更多插件**(可用时)与**创建你的插件**。会话菜单在这些操作之前列出收起的 View,并在当前 View 右侧显示勾选标记。所有菜单都不提供 Pin 或排序操作。 -**浏览更多插件**进入插件页主列表,不打开安装弹窗。**创建你的插件**用 Creator 预设打开新 Session 或可复用的空白 Session,并把对应区域的简短本地化插件创建请求加入草稿:空草稿直接以该请求为文字,已有草稿保留其文字、引用和附件,请求另起一段追加在后面。它绝不发送消息。请自行审阅并发送草稿。当前区域没有可自定义入口时,两项操作仍然可用;空列表不添加分隔线。 +**浏览更多插件**进入插件页主列表,不打开安装弹窗。**创建你的插件**用 Creator 预设打开新 Session 或可复用的空白 Session,并把对应区域的简短本地化插件创建请求加入草稿:空草稿直接以该请求为文字,已有草稿保留其文字、引用和附件,请求另起一段追加在后面。它绝不发送消息。请自行审阅并发送草稿。无论区域是否有入口,创建操作始终可用;分隔线仅显示在收起的 View 与操作之间,浏览可用时位于浏览之前,否则位于创建之前。 -在桌面端,**浏览更多插件**与**创建你的插件**在执行前上报 `more_menu_item_click`,`action` 为 `browse_plugins` 或 `create_plugin`,`region` 为菜单所在区域:**更多**为 `sidebar`,**更多会话视图**为 `conversation`,**更多工具**为 `rightbar`。打开入口和更改 Pin 状态不上报,菜单也不上报曝光事件。没有埋点服务时,两项操作行为不变且不上报。 +在桌面端,**浏览更多插件**与**创建你的插件**在执行前上报 `more_menu_item_click`,`action` 为 `browse_plugins` 或 `create_plugin`,`region` 为菜单所在区域:**更多**为 `sidebar`,**更多会话视图**为 `conversation`,**更多工具**为 `rightbar`。菜单不上报曝光事件。没有埋点服务时,两项操作行为不变且不上报。 -「浏览插件」一行只在 `pluginNavigation` 服务存在时出现;没有该服务时插件照常启动。 +「浏览插件」一行只在 `pluginNavigation` 服务存在时出现;没有该服务时插件照常启动。该服务缺失时仅移除浏览操作,不影响“更多”按钮、收起的 View 或创建操作。 ----- @@ -44,7 +44,7 @@ kind: "package-reference"
实现细节——点击展开 -[`apply`](src/client/index.ts) 通过 `ctx.slots.inject` 以 id `entry-customization` 把 [`EntryMenuContent`](src/client/EntryMenuContent.tsx) 注册到 `sidebar.panellist.actions`、`conversation.session.header.views.actions` 和 `sidebar.right.tab.guide.actions`,每个注册项跟随所在区域声明的生命周期。各区域在自己的 `Menu` 中渲染这些 slot,并以 owner props 提供入口、Pin 状态、`onOpen`、`onPinnedChange` 与 `onDismiss`。菜单行先调用 `onDismiss`,再打开入口或执行底部操作;Pin 变化不关闭菜单。 +[`apply`](src/client/index.ts) 通过 `ctx.slots.inject` 以 id `entry-customization` 把 [`EntryMenuContent`](src/client/EntryMenuContent.tsx) 注册到 `sidebar.panellist.actions`、`conversation.session.header.views.actions` 和 `sidebar.right.tab.guide.actions`,每个注册项跟随所在区域声明的生命周期。各区域在自己的 `Menu` 中渲染这些 slot;菜单行先调用 `onDismiss`,再执行浏览或创建操作。仅会话菜单消费 owner 提供的收起入口列表及导航回调;Pin 回调不被消费。 每个注册项的 `inject` 提供所属区域与两个回调。每个回调先调用 `ctx.get('productAnalytics')?.track('more_menu_item_click', { region, action })`。浏览随后调用 `ctx.get('pluginNavigation')?.openList()`;ui-plugin-manager 只在插件页有权限时提供该服务,因此本包按可选方式读取,不把它写进 `inject`。创建随后按区域取本地化提示语,调用 `uiWorkspace.startSession(undefined, { agentPreset: 'cordis', prompt, whenDraftExists: 'append' })`;该调用不经过 `ui-agent-preset`,预设标签不播放介绍动画。[`dsh-web-app` bundle patch](../../bundle/web-app/cordis.patch.yml) 挂载浏览器插件。 @@ -65,12 +65,12 @@ kind: "package-reference" -- 用户收起入口后,若本包被停用或加载失败,被收起的入口保持隐藏,且没有“更多”菜单可以恢复。重新启用本包或清除对应区域的 `dsh.slot.manual-order.*.v1` localStorage 键即可恢复。 -- Pin 状态保存在浏览器 localStorage,按协议、主机和端口隔离:更换 Web 端口或在 Web 与 Desktop 之间切换时不共享,也不跨设备同步。 +- 更多菜单不提供入口显隐管理。其他注册项仍可调用区域 Pin 回调,仅修改内存中的选择;插件重新加载后恢复默认值。 +- 不读取或写入 localStorage 中的旧 Pin 值。 - **创建你的插件**不检查 Creator 预设是否存在。预设缺失时 Session 创建失败,Workspace 显示创建失败提示,不准备草稿。草稿准备还要求可确定目标 Workspace;没有目标时,现有流程提示需要 Workspace。 -- 入口保持注册顺序;菜单不提供排序,入口也不能跨区域移动。 +- 会话区域负责按估算值分组;菜单不提供排序。 - 会话控件遵循现有非空 Session 头部与代码工作工具的可用性规则。右侧栏控件要求已选择 Session,并显示在其起始页。 -- 仅显示已激活、已注册的入口;菜单不是已停用或未安装插件的清单。 +- 菜单不列出插件清单;使用浏览操作进入插件页。 ### 开发备注 diff --git a/packages/client/ui-entry-customization/locale/de.json b/packages/client/ui-entry-customization/locale/de.json new file mode 100644 index 0000000000..b00350d66a --- /dev/null +++ b/packages/client/ui-entry-customization/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Anpassung der Benutzeroberfläche", + "description": "Passen Sie Plugin-Einträge in der linken Seitenleiste, den Konversationsansichten und der rechten Seitenleiste an und erstellen Sie schnell neue Plugins." + } +} diff --git a/packages/client/ui-entry-customization/locale/es.json b/packages/client/ui-entry-customization/locale/es.json new file mode 100644 index 0000000000..c91bc568a9 --- /dev/null +++ b/packages/client/ui-entry-customization/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Personalización de la interfaz", + "description": "Personalice las entradas de complementos en la barra lateral izquierda, las vistas de conversación y la barra lateral derecha, y cree rápidamente nuevos complementos." + } +} diff --git a/packages/client/ui-entry-customization/locale/fr.json b/packages/client/ui-entry-customization/locale/fr.json new file mode 100644 index 0000000000..b5209dccd1 --- /dev/null +++ b/packages/client/ui-entry-customization/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Personnalisation de l'interface", + "description": "Personnalisez les entrées de plugin dans la barre latérale gauche, les vues de conversation et la barre latérale droite, et créez rapidement de nouveaux plugins." + } +} diff --git a/packages/client/ui-entry-customization/locale/pt.json b/packages/client/ui-entry-customization/locale/pt.json new file mode 100644 index 0000000000..d0171b4721 --- /dev/null +++ b/packages/client/ui-entry-customization/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Personalização da interface", + "description": "Personalize as entradas de plug-ins na barra lateral esquerda, nas visualizações de conversas e na barra lateral direita e crie novos plug-ins rapidamente." + } +} diff --git a/packages/client/ui-entry-customization/locale/ru.json b/packages/client/ui-entry-customization/locale/ru.json new file mode 100644 index 0000000000..52ba475bb5 --- /dev/null +++ b/packages/client/ui-entry-customization/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Настройка интерфейса", + "description": "Настраивайте записи плагинов на левой боковой панели, в представлениях бесед и на правой боковой панели и быстро создавайте новые плагины." + } +} diff --git a/packages/client/ui-entry-customization/src/client/EntryMenu.module.css b/packages/client/ui-entry-customization/src/client/EntryMenu.module.css index 82ce8f3f88..d7d1c3a298 100644 --- a/packages/client/ui-entry-customization/src/client/EntryMenu.module.css +++ b/packages/client/ui-entry-customization/src/client/EntryMenu.module.css @@ -1,12 +1,3 @@ -.entryRow { position: relative; } -.entryName { display: flex; align-items: center; gap: 8px; padding-right: 28px; } +.entryName { display: flex; align-items: center; gap: 8px; } .entryTitle { min-width: 0; overflow-wrap: anywhere; white-space: normal; } -.singleLine .entryTitle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.pinAnchor { position: absolute; right: 6px; top: 0; bottom: 0; display: flex; align-items: center; } -.pinButton { width: 22px; height: 22px; padding: 0; color: var(--dsw-alias-label-tertiary); } - -@media (hover: hover) { - .pinButton:hover { color: var(--dsw-alias-label-primary); } - .pinAnchor[data-pinned='false'] { opacity: 0; } - .entryRow:hover .pinAnchor, .entryRow:focus-within .pinAnchor { opacity: 1; } -} +.activeMark { flex: none; margin-left: auto; } diff --git a/packages/client/ui-entry-customization/src/client/EntryMenuContent.tsx b/packages/client/ui-entry-customization/src/client/EntryMenuContent.tsx index 22a73babc1..5ab4fa91cf 100644 --- a/packages/client/ui-entry-customization/src/client/EntryMenuContent.tsx +++ b/packages/client/ui-entry-customization/src/client/EntryMenuContent.tsx @@ -1,17 +1,17 @@ -/** Rows of a navigation region's More menu: customizable entries, plugin discovery while reachable, and creation. */ +/** Folded Conversation Views, plugin discovery while reachable, and creation. */ import { - IconPluginPinwheelOutlineRegular, IconPlusOutlineRegular, MenuItemButton, + IconCheckOutlineRegular, IconPluginPinwheelOutlineRegular, IconPlusOutlineRegular, MenuItemButton, } from '@deepseek-ai/dsh-client-ui-primitives' import type { HostObservable, InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client' -import { EntryPickerRow } from './EntryPickerRow.tsx' +import css from './EntryMenu.module.css' /** Navigation region whose More menu hosts one registration. */ export type EntryRegion = 'sidebar' | 'conversation' | 'rightbar' /** Region identity and footer actions injected into each registration. */ export interface EntryMenuInjected { - /** Region of this registration; selects pin copy and the Creator prompt. */ + /** Region of this registration; selects analytics attributes and the Creator prompt. */ readonly region: EntryRegion readonly hooks: { /** Whether the Plugins page navigation service is present; the discovery row shows only while it is. */ @@ -28,27 +28,28 @@ type EntryMenuContentProps = PropsRuntime<'sidebar.panellist.actions'> & PropsLocale<'entryCustomization'> & InjectFace /** - * Render the region's entries with pin actions, then the discovery and Creator rows. - * @param props - Owner entries and callbacks, injected region actions, and localized copy. + * Render folded Conversation Views followed by discovery and Creator actions, without pin controls. + * @param props - Owner dismissal callback, injected actions, and localized copy. * @returns Menu rows for the region's More menu. */ export function EntryMenuContent({ - entries, onOpen, onPinnedChange, onDismiss, region, usePluginNavigation, browsePlugins, createPlugin, t, + entries, onOpen, region, onDismiss, usePluginNavigation, browsePlugins, createPlugin, t, }: EntryMenuContentProps) { + const moreEntries = region === 'conversation' ? entries : [] const browsable = usePluginNavigation(present => present) return <> - {entries.map((entry) => { - const pinLabel = t(`${entry.pinned ? 'unpin' : 'pin'}.${region}`) - return { onDismiss(); onOpen(entry.id) }} - onPin={() => { onPinnedChange(entry.id, !entry.pinned) }} /> - })} - {browsable && } separatorBefore={entries.length > 0} + {moreEntries.map(entry => { onDismiss(); onOpen(entry.id) }}> + + {entry.label} + {entry.active === true && } + + )} + {browsable && 0} icon={} onSelect={() => { onDismiss(); browsePlugins() }}> {t('install')} } - } separatorBefore={!browsable && entries.length > 0} + } separatorBefore={!browsable && moreEntries.length > 0} onSelect={() => { onDismiss(); createPlugin() }}> {t('create')} diff --git a/packages/client/ui-entry-customization/src/client/EntryPickerRow.tsx b/packages/client/ui-entry-customization/src/client/EntryPickerRow.tsx deleted file mode 100644 index 4148de08f9..0000000000 --- a/packages/client/ui-entry-customization/src/client/EntryPickerRow.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** A launcher row separates navigation from its independently focusable pin action. */ -import clsx from 'clsx' -import { - Button, IconPinFillRegular, IconPinOutlineRegular, MenuItemButton, Tooltip, -} from '@deepseek-ai/dsh-client-ui-primitives' -import css from './EntryMenu.module.css' - -interface EntryPickerRowProps { - readonly label: string - readonly pinned: boolean - readonly pinLabel: string - readonly pinAccessibleLabel: string - /** Truncate the label to one line instead of wrapping it. */ - readonly singleLine: boolean - readonly onOpen: () => void - readonly onPin: () => void -} - -/** - * Render two sibling controls so pinning never opens the entry. - * @param props - Entry label, pin state, localized action names and independent callbacks. - * @returns A shared menu row with a trailing pin button. - */ -export function EntryPickerRow({ - label, pinned, pinLabel, pinAccessibleLabel, singleLine, onOpen, onPin, -}: EntryPickerRowProps) { - return
- - - {label} - - - - - - - -
-} diff --git a/packages/client/ui-entry-customization/src/client/index.ts b/packages/client/ui-entry-customization/src/client/index.ts index ba41d131a9..689279b22c 100644 --- a/packages/client/ui-entry-customization/src/client/index.ts +++ b/packages/client/ui-entry-customization/src/client/index.ts @@ -10,11 +10,11 @@ import type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client' import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client' import type {} from '@deepseek-ai/dsh-client-ui-workspace/client' import { EntryMenuContent, type EntryMenuInjected, type EntryRegion } from './EntryMenuContent.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { - /** More menu rows: pin actions, footer actions, and Creator draft prompts. */ + /** More menu rows: navigation actions, and Creator draft prompts. */ entryCustomization: keyof typeof en } } @@ -27,7 +27,7 @@ export const inject = ['slots', 'locale', 'uiWorkspace'] * @param ctx - Browser context with slot, locale, and Workspace services, and the optional plugin navigation. */ export function apply(ctx: Context): void { - ctx.effect(() => ctx.locale.register('entryCustomization', { en, zh, ja })) + ctx.effect(() => ctx.locale.register('entryCustomization', { en, zh, ja, es, fr, de, ru, pt })) const t = ctx.locale.bind('entryCustomization') // The Plugins page provides its navigation only while the current Peer may use it. const pluginNavigation = createSnapshotStore(false) diff --git a/packages/client/ui-entry-customization/src/client/locales.ts b/packages/client/ui-entry-customization/src/client/locales.ts index 6693ed6fc2..6290a0e278 100644 --- a/packages/client/ui-entry-customization/src/client/locales.ts +++ b/packages/client/ui-entry-customization/src/client/locales.ts @@ -1,12 +1,5 @@ -/** Copy for the More menu rows: pin actions, footer actions, and Creator draft prompts per region. */ +/** Copy for the More menu rows: navigation actions, and Creator draft prompts per region. */ export const en = { - 'pin.sidebar': 'Pin to left sidebar', - 'unpin.sidebar': 'Unpin from left sidebar', - 'pin.conversation': 'Pin to conversation tabs', - 'unpin.conversation': 'Unpin from conversation tabs', - 'pin.rightbar': 'Pin to right sidebar', - 'unpin.rightbar': 'Unpin from right sidebar', - pinLabel: '{action}: {name}', install: 'Browse other plugins', create: 'Create your plugin', 'createPrompt.sidebar': 'Create a DSH plugin with a left sidebar entry.', @@ -16,13 +9,6 @@ export const en = { /** Chinese counterpart of the entry-customization dictionary. */ export const zh: Record = { - 'pin.sidebar': '固定到左侧栏', - 'unpin.sidebar': '从左侧栏取消固定', - 'pin.conversation': '固定到会话标签栏', - 'unpin.conversation': '从会话标签栏取消固定', - 'pin.rightbar': '固定到右侧栏', - 'unpin.rightbar': '从右侧栏取消固定', - pinLabel: '{action}:{name}', install: '浏览更多插件', create: '创建你的插件', 'createPrompt.sidebar': '创建一个带左侧栏入口的 DSH 插件。', @@ -32,16 +18,89 @@ export const zh: Record = { /** Japanese copy, checked against the English keys. */ export const ja = { - 'pin.sidebar': '左サイドバーに固定', - 'unpin.sidebar': '左サイドバーから固定解除', - 'pin.conversation': '会話タブに固定', - 'unpin.conversation': '会話タブから固定解除', - 'pin.rightbar': '右サイドバーに固定', - 'unpin.rightbar': '右サイドバーから固定解除', - 'pinLabel': '{action}: {name}', 'install': '他のプラグインを探す', 'create': 'プラグインを作成', 'createPrompt.sidebar': '左サイドバーにエントリを持つ DSH プラグインを作成します。', 'createPrompt.conversation': 'DSH 会話ビュープラグインを作成します。', 'createPrompt.rightbar': '右サイドバーにエントリを持つ DSH プラグインを作成します。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'pin.sidebar': 'Fijar a la barra lateral izquierda', + 'unpin.sidebar': 'Desanclar de la barra lateral izquierda', + 'pin.conversation': 'Anclar a pestañas de conversación', + 'unpin.conversation': 'Desanclar de las pestañas de conversación', + 'pin.rightbar': 'Fijar a la barra lateral derecha', + 'unpin.rightbar': 'Desanclar de la barra lateral derecha', + pinLabel: '{action}: {name}', + install: 'Explorar otros complementos', + create: 'Crea tu complemento', + 'createPrompt.sidebar': 'Cree un complemento DSH con una entrada en la barra lateral izquierda.', + 'createPrompt.conversation': 'Cree un complemento de vista de conversación DSH.', + 'createPrompt.rightbar': 'Cree un complemento DSH con una entrada en la barra lateral derecha.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'pin.sidebar': 'Épingler sur la barre latérale gauche', + 'unpin.sidebar': "Retirer l'épingle de la barre latérale gauche", + 'pin.conversation': 'Épingler aux onglets de conversation', + 'unpin.conversation': "Retirer l'épingle des onglets de conversation", + 'pin.rightbar': 'Épingler sur la barre latérale droite', + 'unpin.rightbar': "Retirer l'épingle de la barre latérale droite", + pinLabel: '{action} : {name}', + install: "Parcourir d'autres plugins", + create: 'Créez votre plugin', + 'createPrompt.sidebar': 'Créez un plugin DSH avec une entrée dans la barre latérale gauche.', + 'createPrompt.conversation': "Créez un plugin d'affichage de conversation DSH.", + 'createPrompt.rightbar': 'Créez un plugin DSH avec une entrée dans la barre latérale droite.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'pin.sidebar': 'An die linke Seitenleiste anpinnen', + 'unpin.sidebar': 'Von der linken Seitenleiste lösen', + 'pin.conversation': 'An Konversationsregisterkarten anpinnen', + 'unpin.conversation': 'Von Konversations-Tabs lösen', + 'pin.rightbar': 'An rechte Seitenleiste anheften', + 'unpin.rightbar': 'Von der rechten Seitenleiste lösen', + pinLabel: '{action}: {name}', + install: 'Durchsuchen Sie andere Plugins', + create: 'Erstellen Sie Ihr Plugin', + 'createPrompt.sidebar': 'Erstellen Sie ein DSH-Plugin mit einem Eintrag in der linken Seitenleiste.', + 'createPrompt.conversation': 'Erstellen Sie ein DSH-Plugin für die Konversationsansicht.', + 'createPrompt.rightbar': 'Erstellen Sie ein DSH-Plugin mit einem Eintrag in der rechten Seitenleiste.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'pin.sidebar': 'Закрепить на левой боковой панели', + 'unpin.sidebar': 'Открепить от левой боковой панели', + 'pin.conversation': 'Закрепить на вкладках беседы', + 'unpin.conversation': 'Открепить от вкладок беседы', + 'pin.rightbar': 'Закрепить на правой боковой панели', + 'unpin.rightbar': 'Открепить от правой боковой панели', + pinLabel: '{action}: {name}', + install: 'Просмотрите другие плагины', + create: 'Создайте свой плагин', + 'createPrompt.sidebar': 'Создайте плагин DSH с записью на левой боковой панели.', + 'createPrompt.conversation': 'Создайте плагин просмотра диалога DSH.', + 'createPrompt.rightbar': 'Создайте плагин DSH с записью на правой боковой панели.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'pin.sidebar': 'Fixar na barra lateral esquerda', + 'unpin.sidebar': 'Soltar da barra lateral esquerda', + 'pin.conversation': 'Fixar nas guias de conversa', + 'unpin.conversation': 'Liberar das guias de conversa', + 'pin.rightbar': 'Fixar na barra lateral direita', + 'unpin.rightbar': 'Soltar da barra lateral direita', + pinLabel: '{action}: {name}', + install: 'Procure outros plug-ins', + create: 'Crie seu plug-in', + 'createPrompt.sidebar': 'Crie um plug-in DSH com uma entrada na barra lateral esquerda.', + 'createPrompt.conversation': 'Crie um plug-in de visualização de conversa DSH.', + 'createPrompt.rightbar': 'Crie um plug-in DSH com uma entrada na barra lateral direita.', +} satisfies Record diff --git a/packages/client/ui-entry-customization/tests/menu.client.spec.tsx b/packages/client/ui-entry-customization/tests/menu.client.spec.tsx index 1a38e84430..ff77c49ca7 100644 --- a/packages/client/ui-entry-customization/tests/menu.client.spec.tsx +++ b/packages/client/ui-entry-customization/tests/menu.client.spec.tsx @@ -1,5 +1,5 @@ // @vitest-environment jsdom -/** The plugin fills each region's More menu with entry rows and the discovery and Creator actions. */ +/** The plugin fills each region's More menu with the discovery and Creator actions. */ import type { Context } from '@deepseek-ai/cordis' import { afterEach, describe, expect, it, vi } from 'vitest' import { act, cleanup, fireEvent, render, within } from '@testing-library/react' @@ -30,10 +30,9 @@ function content(overrides: Partial = {}) { onDismiss: vi.fn(), browsePlugins: vi.fn(), createPlugin: vi.fn(), } - // Framework hooks stay unused stubs; this isolated row set reads only its owner, inject, and locale shares. const props = { entries: [{ id: 'notes', label: 'Notes', pinned: true }, { id: 'tasks', label: 'Tasks', pinned: false }], - region: 'sidebar', t: makeTranslate(en), renderFactorySlot: unused, + region: 'sidebar', t: makeTranslate(en), usePluginNavigation: (select: (present: boolean) => T): T => select(true), usePanelInfo: unused, useSessions: unused, useSessionStatus: unused, useSessionRetainInfo: unused, useResource: unused, useWorkspaces: unused, @@ -45,54 +44,59 @@ function content(overrides: Partial = {}) { } describe('More menu rows', () => { - it('separates opening from pinning and reports the inverted pin state', () => { - const b = content() - expect(b.view.getAllByRole('menuitem').map(row => row.getAttribute('aria-label') ?? row.textContent)).toEqual([ - 'Notes', 'Unpin from left sidebar: Notes', 'Tasks', 'Pin to left sidebar: Tasks', 'Browse other plugins', 'Create your plugin', - ]) - expect(b.view.getAllByRole('separator')).toHaveLength(1) - fireEvent.click(b.view.getByRole('menuitem', { name: 'Unpin from left sidebar: Notes' })) - fireEvent.click(b.view.getByRole('menuitem', { name: 'Pin to left sidebar: Tasks' })) - expect(b.onPinnedChange).toHaveBeenNthCalledWith(1, 'notes', false) - expect(b.onPinnedChange).toHaveBeenNthCalledWith(2, 'tasks', true) - expect(b.onDismiss).not.toHaveBeenCalled() - expect(b.onOpen).not.toHaveBeenCalled() - fireEvent.click(b.view.getByRole('menuitem', { name: 'Tasks' })) - expect(b.onDismiss).toHaveBeenCalledOnce() - expect(b.onOpen).toHaveBeenCalledExactlyOnceWith('tasks') - expect(b.onDismiss.mock.invocationCallOrder[0]).toBeLessThan(b.onOpen.mock.invocationCallOrder[0]!) - }) - - it.each([ - ['conversation', 'Pin to conversation tabs: Tasks'], - ['rightbar', 'Pin to right sidebar: Tasks'], - ] as const)('names the %s pin destination', (region, name) => { - const { view } = content({ region }) - expect(view.getByRole('menuitem', { name })).toBeTruthy() - }) - - it('keeps only the discovery and Creator actions, without a separator, for an empty roster', () => { - const b = content({ entries: [], region: 'rightbar' }) + it.each(['sidebar', 'rightbar'] as const)('shows only discovery and creation in %s regardless of entries', (region) => { + const b = content({ region, entries: [ + { id: 'notes', label: 'Notes', pinned: true, active: true }, + { id: 'tasks', label: 'Tasks', pinned: false }, + ] }) expect(b.view.getAllByRole('menuitem').map(row => row.textContent)).toEqual(['Browse other plugins', 'Create your plugin']) expect(b.view.queryByRole('separator')).toBeNull() fireEvent.click(b.view.getByRole('menuitem', { name: 'Browse other plugins' })) expect(b.browsePlugins).toHaveBeenCalledExactlyOnceWith() + expect(b.onDismiss.mock.invocationCallOrder[0]).toBeLessThan(b.browsePlugins.mock.invocationCallOrder[0]!) fireEvent.click(b.view.getByRole('menuitem', { name: 'Create your plugin' })) expect(b.createPlugin).toHaveBeenCalledExactlyOnceWith() - expect(b.onDismiss).toHaveBeenCalledTimes(2) + expect(b.onDismiss.mock.invocationCallOrder[1]).toBeLessThan(b.createPlugin.mock.invocationCallOrder[0]!) + expect(b.onOpen).not.toHaveBeenCalled() + expect(b.onPinnedChange).not.toHaveBeenCalled() }) - it('shows the pin hint after the shared tooltip delay outside the menu card', () => { - vi.stubGlobal('matchMedia', vi.fn((query: string) => ({ matches: query === '(any-hover: hover)' }))) - const { view } = content() - const unpin = view.getByRole('menuitem', { name: 'Unpin from left sidebar: Notes' }) - vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) - fireEvent.pointerEnter(unpin, { pointerType: 'mouse' }) - act(() => { vi.advanceTimersByTime(499) }) - expect(view.queryByRole('tooltip', { hidden: true })).toBeNull() - act(() => { vi.advanceTimersByTime(1) }) - expect(view.getByRole('tooltip', { hidden: true }).closest('[role="menu"]')).toBeNull() - fireEvent.pointerLeave(unpin, { pointerType: 'mouse' }) + it.each([true, false])('opens folded Views and marks the active View with plugin navigation present=%s', (browsable) => { + const b = content({ region: 'conversation', + usePluginNavigation: select => select(browsable), + entries: [ + { id: 'notes', label: 'Notes', pinned: true, active: true }, + { id: 'tasks', label: 'Tasks', pinned: true, active: false }, + ], + }) + expect(b.view.getAllByRole('menuitem').map(row => row.textContent)) + .toEqual(['Notes', 'Tasks', ...(browsable ? ['Browse other plugins'] : []), 'Create your plugin']) + const currentRow = b.view.getByRole('menuitem', { name: 'Notes' }) + expect(currentRow.getAttribute('aria-current')).toBe('page') + expect(currentRow.querySelector('svg')?.previousElementSibling?.textContent).toBe('Notes') + expect(b.view.getByRole('menuitem', { name: 'Tasks' }).hasAttribute('aria-current')).toBe(false) + expect(b.view.getAllByRole('separator')).toHaveLength(1) + fireEvent.click(b.view.getByRole('menuitem', { name: 'Tasks' })) + expect(b.onOpen).toHaveBeenCalledExactlyOnceWith('tasks') + expect(b.onDismiss.mock.invocationCallOrder[0]).toBeLessThan(b.onOpen.mock.invocationCallOrder[0]!) + expect(b.onPinnedChange).not.toHaveBeenCalled() + }) + + it.each([true, false])('keeps actions unseparated for an empty roster with plugin navigation present=%s', (browsable) => { + const b = content({ region: 'conversation', entries: [], usePluginNavigation: select => select(browsable) }) + expect(b.view.getAllByRole('menuitem').map(row => row.textContent)) + .toEqual([...(browsable ? ['Browse other plugins'] : []), 'Create your plugin']) + expect(b.view.queryByRole('separator')).toBeNull() + }) + + it.each(['sidebar', 'rightbar'] as const)('keeps only creation in %s without plugin navigation', (region) => { + const b = content({ region, usePluginNavigation: select => select(false) }) + expect(b.view.getAllByRole('menuitem').map(row => row.textContent)).toEqual(['Create your plugin']) + expect(b.view.queryByRole('separator')).toBeNull() + fireEvent.click(b.view.getByRole('menuitem', { name: 'Create your plugin' })) + expect(b.createPlugin).toHaveBeenCalledExactlyOnceWith() + expect(b.onOpen).not.toHaveBeenCalled() + expect(b.onPinnedChange).not.toHaveBeenCalled() }) }) @@ -103,7 +107,7 @@ function Frame({ renderSlot }: PropsRenderSlots) { return <> {SEATS.map(seat =>
{renderSlot(seat, { - entries, + entries: seat === 'conversation.session.header.views.actions' ? [] : entries, onOpen: (id) => { setOpened(`${seat}:${id}`) }, onPinnedChange: (_id, next) => { setPinned(next) }, onDismiss: () => {}, @@ -148,8 +152,7 @@ describe('More menu registrations', () => { const sidebar = () => within(b.view.getByRole('menu', { name: 'sidebar.panellist.actions' })) expect(sidebar().queryByRole('menuitem', { name: 'Browse other plugins' })).toBeNull() expect(sidebar().getByRole('menuitem', { name: 'Create your plugin' })).toBeTruthy() - // The Creator row takes the separator the absent discovery row would carry. - expect(sidebar().getAllByRole('separator')).toHaveLength(1) + expect(sidebar().queryByRole('separator')).toBeNull() const injected = b.runtime.slots.entries('sidebar.panellist.actions')[0]!.inject!() as { browsePlugins: () => void } expect(() => { injected.browsePlugins() }).not.toThrow() @@ -170,15 +173,10 @@ describe('More menu registrations', () => { const b = await bench() for (const seat of SEATS) { expect(b.runtime.slots.entries(seat).map(entry => entry.options.id)).toEqual(['entry-customization']) - expect(within(b.view.getByRole('menu', { name: seat })).getByRole('menuitem', { name: 'Notes' })).toBeTruthy() + expect(within(b.view.getByRole('menu', { name: seat })).getAllByRole('menuitem').map(row => row.textContent)) + .toEqual(['Browse other plugins', 'Create your plugin']) } const sidebar = within(b.view.getByRole('menu', { name: 'sidebar.panellist.actions' })) - fireEvent.click(sidebar.getByRole('menuitem', { name: 'Unpin from left sidebar: Notes' })) - expect(within(b.view.getByRole('menu', { name: 'sidebar.right.tab.guide.actions' })) - .getByRole('menuitem', { name: 'Pin to right sidebar: Notes' })).toBeTruthy() - fireEvent.click(within(b.view.getByRole('menu', { name: 'conversation.session.header.views.actions' })) - .getByRole('menuitem', { name: 'Notes' })) - expect(b.view.getByTestId('opened').textContent).toBe('conversation.session.header.views.actions:notes') fireEvent.click(sidebar.getByRole('menuitem', { name: 'Browse other plugins' })) expect(b.openList).toHaveBeenCalledExactlyOnceWith() await b.plugin.dispose() @@ -220,15 +218,6 @@ describe('More menu registrations', () => { expect(b.track).toHaveBeenCalledTimes(2) }) - it('reports neither entry opening nor pin changes', async () => { - const b = await bench() - const menu = within(b.view.getByRole('menu', { name: 'sidebar.panellist.actions' })) - fireEvent.click(menu.getByRole('menuitem', { name: 'Unpin from left sidebar: Notes' })) - fireEvent.click(menu.getByRole('menuitem', { name: 'Notes' })) - expect(b.view.getByTestId('opened').textContent).toBe('sidebar.panellist.actions:notes') - expect(b.track).not.toHaveBeenCalled() - }) - it('keeps both footer actions working without the analytics service', async () => { const b = await bench('en', { analytics: false }) const menu = within(b.view.getByRole('menu', { name: 'sidebar.right.tab.guide.actions' })) diff --git a/packages/client/ui-entry-customization/tests/pin-styles.client.spec.ts b/packages/client/ui-entry-customization/tests/pin-styles.client.spec.ts deleted file mode 100644 index c30d3ee705..0000000000 --- a/packages/client/ui-entry-customization/tests/pin-styles.client.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** Pin controls use Session-action ink without changing the navigation control's dimensions. */ -import { readFileSync } from 'node:fs' -import { describe, expect, it } from 'vitest' - -const css = readFileSync(new URL('../src/client/EntryMenu.module.css', import.meta.url), 'utf8') - -describe('entry pin styling', () => { - it('uses tertiary ink for both pin states and primary ink on pointer hover', () => { - expect(css).toMatch(/\.pinButton\s*\{[^}]*color:\s*var\(--dsw-alias-label-tertiary\)/) - expect(css).toMatch(/\.pinButton:hover\s*\{[^}]*color:\s*var\(--dsw-alias-label-primary\)/) - expect(css).not.toMatch(/\[data-pinned=['"]true['"]\]\s+\.pinButton\s*\{[^}]*color:/) - }) - - it('centers the pin without creating a transformed tooltip container', () => { - const pinAnchor = css.match(/\.pinAnchor\s*\{([^}]*)\}/)?.[1] - expect(pinAnchor).toBeDefined() - expect(pinAnchor).toContain('align-items: center') - expect(pinAnchor).not.toContain('transform:') - }) -}) diff --git a/packages/client/ui-file-drag/locale/de.json b/packages/client/ui-file-drag/locale/de.json new file mode 100644 index 0000000000..5f610493c3 --- /dev/null +++ b/packages/client/ui-file-drag/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Ziehen von Dateien", + "description": "Ziehen Sie Originaldateien in den Composer oder andere Anwendungen, mit lokalisierter Fehlerrückmeldung." + } +} diff --git a/packages/client/ui-file-drag/locale/es.json b/packages/client/ui-file-drag/locale/es.json new file mode 100644 index 0000000000..ff04cd6d4c --- /dev/null +++ b/packages/client/ui-file-drag/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Arrastrar archivos", + "description": "Arrastre los archivos originales al compositor u otras aplicaciones, con comentarios de falla localizados." + } +} diff --git a/packages/client/ui-file-drag/locale/fr.json b/packages/client/ui-file-drag/locale/fr.json new file mode 100644 index 0000000000..302a536b7d --- /dev/null +++ b/packages/client/ui-file-drag/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Glisser un fichier", + "description": "Faites glisser les fichiers originaux dans le composeur ou d'autres applications, avec un retour d'échec localisé." + } +} diff --git a/packages/client/ui-file-drag/locale/pt.json b/packages/client/ui-file-drag/locale/pt.json new file mode 100644 index 0000000000..9d59d7c683 --- /dev/null +++ b/packages/client/ui-file-drag/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Arrastando arquivo", + "description": "Arraste os arquivos originais para o compositor ou outros aplicativos, com feedback de falha localizado." + } +} diff --git a/packages/client/ui-file-drag/locale/ru.json b/packages/client/ui-file-drag/locale/ru.json new file mode 100644 index 0000000000..00cd72b430 --- /dev/null +++ b/packages/client/ui-file-drag/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Перетаскивание файлов", + "description": "Перетащите исходные файлы в композитор или другие приложения с локализованной обратной связью об ошибках." + } +} diff --git a/packages/client/ui-file-drag/src/client/index.ts b/packages/client/ui-file-drag/src/client/index.ts index efe3fb7eab..cfc9c1c85c 100644 --- a/packages/client/ui-file-drag/src/client/index.ts +++ b/packages/client/ui-file-drag/src/client/index.ts @@ -8,7 +8,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-layout/client' import type {} from '@deepseek-ai/dsh-client-locale/client' import { FileDragFeedback, type FileDragFailure, type FileDragFeedbackInjected } from './feedback.tsx' import { FileTransfers } from './transfers.ts' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' /** Application-local file preparation and drop resolution. */ export interface FileDragService { @@ -33,7 +33,7 @@ export function apply(ctx: Context): void { const desktop = bridge?.protocolVersion === 1 ? bridge.fileDrag : undefined const failure = createSnapshotStore(null) const t = ctx.locale.bind('fileDrag') - ctx.effect(() => ctx.locale.register('fileDrag', { zh, en, ja })) + ctx.effect(() => ctx.locale.register('fileDrag', { zh, en, ja, es, fr, de, ru, pt })) let sequence = 0 const reportFailure = (): void => { failure.set({ text: t('failed'), sequence: ++sequence }) } const transfers = new FileTransfers(desktop, reportFailure) diff --git a/packages/client/ui-file-drag/src/client/locales.ts b/packages/client/ui-file-drag/src/client/locales.ts index 9497b29dbc..d0da371eca 100644 --- a/packages/client/ui-file-drag/src/client/locales.ts +++ b/packages/client/ui-file-drag/src/client/locales.ts @@ -7,6 +7,21 @@ export const en = { failed: 'Drag failed. Try again.' } satisfies Record +/** Spanish dictionary, checked against the English keys. */ +export const es = { failed: 'Error al arrastrar. Intentar otra vez.' } satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { failed: 'Le glissement a échoué. Essayer à nouveau.' } satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { failed: 'Ziehen ist fehlgeschlagen. Versuchen Sie es erneut.' } satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { failed: 'Перетащить не удалось. Попробуйте еще раз.' } satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { failed: 'Falha ao arrastar. Tente novamente.' } satisfies Record + declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { /** File transfer failure feedback. */ diff --git a/packages/client/ui-goal/locale/de.json b/packages/client/ui-goal/locale/de.json new file mode 100644 index 0000000000..2a2420166b --- /dev/null +++ b/packages/client/ui-goal/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Sitzungszielleiste", + "description": "Zeigt das Ziel und den Status der aktuellen Sitzung über dem Eingabefeld an." + } +} diff --git a/packages/client/ui-goal/locale/es.json b/packages/client/ui-goal/locale/es.json new file mode 100644 index 0000000000..e0135182ed --- /dev/null +++ b/packages/client/ui-goal/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "barra de destino de sesión", + "description": "Muestra el objetivo y el estado de la sesión actual encima del cuadro de entrada." + } +} diff --git a/packages/client/ui-goal/locale/fr.json b/packages/client/ui-goal/locale/fr.json new file mode 100644 index 0000000000..2282fdbd25 --- /dev/null +++ b/packages/client/ui-goal/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "barre de cible de session", + "description": "Affiche l'objectif et le statut de la session en cours au-dessus de la zone de saisie." + } +} diff --git a/packages/client/ui-goal/locale/pt.json b/packages/client/ui-goal/locale/pt.json new file mode 100644 index 0000000000..d7e3fa30c3 --- /dev/null +++ b/packages/client/ui-goal/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "barra de destino da sessão", + "description": "Exibe a meta e o status da sessão atual acima da caixa de entrada." + } +} diff --git a/packages/client/ui-goal/locale/ru.json b/packages/client/ui-goal/locale/ru.json new file mode 100644 index 0000000000..2736841f94 --- /dev/null +++ b/packages/client/ui-goal/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "целевая панель сеанса", + "description": "Отображает цель и статус текущего сеанса над полем ввода." + } +} diff --git a/packages/client/ui-goal/src/client/index.ts b/packages/client/ui-goal/src/client/index.ts index a38de98032..7c1eadb38a 100644 --- a/packages/client/ui-goal/src/client/index.ts +++ b/packages/client/ui-goal/src/client/index.ts @@ -35,7 +35,7 @@ import { GoalCommandReference } from './GoalCommandReference.tsx' import { goalCommandInputDefinition } from './goal-command-input.ts' import { goalCommandReferenceRule } from './user-text-rules.ts' export type {} from './user-text-rules.ts' -import { en, ja, zh, type GoalKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type GoalKey } from './locales.ts' export { GoalBar, GoalDock } from './GoalBar.tsx' export type { @@ -70,7 +70,7 @@ export const inject = ['slots', 'sessions', 'remote', 'remote.goals', 'locale', export function apply(ctx: ClientContext): void { ctx.uiConversation.events.register(goalCommandInputDefinition) ctx.uiConversation.userText.register(goalCommandReferenceRule) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-goal: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-goal: dictionaries') ctx.slots.inject('conversation.user.text.node', () => ctx.slots.register({ name: 'conversation.user.text.node', diff --git a/packages/client/ui-goal/src/client/locales.ts b/packages/client/ui-goal/src/client/locales.ts index b196c8c7a9..f52946329e 100644 --- a/packages/client/ui-goal/src/client/locales.ts +++ b/packages/client/ui-goal/src/client/locales.ts @@ -50,3 +50,83 @@ export const ja = { 'action.edit': '目標を編集', 'action.clear': '目標をクリア', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'phase.active': 'Meta en curso', + 'phase.active.disarmed': 'Meta inactiva', + 'phase.paused': 'Objetivo en pausa', + 'phase.blocked': 'Meta bloqueada', + 'objective.aria': 'Objetivo de meta', + 'commandInput.aria': 'Entrada de comando', + 'action.save': 'Guardar gol', + 'action.cancel': 'Cancelar edición', + 'action.pause': 'Pausar objetivo', + 'action.resume': 'Reanudar objetivo', + 'action.edit': 'Editar objetivo', + 'action.clear': 'Objetivo claro', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'phase.active': 'Objectif continu', + 'phase.active.disarmed': 'Objectif inactif', + 'phase.paused': 'Objectif suspendu', + 'phase.blocked': 'Objectif bloqué', + 'objective.aria': 'Objectif du but', + 'commandInput.aria': 'Entrée de commande', + 'action.save': "Sauvegarder l'objectif", + 'action.cancel': 'Annuler la modification', + 'action.pause': "Suspendre l'objectif", + 'action.resume': "Reprendre l'objectif", + 'action.edit': "Modifier l'objectif", + 'action.clear': 'Objectif clair', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'phase.active': 'Fortlaufendes Ziel', + 'phase.active.disarmed': 'Inaktives Ziel', + 'phase.paused': 'Pausiertes Ziel', + 'phase.blocked': 'Blockiertes Ziel', + 'objective.aria': 'Zielziel', + 'commandInput.aria': 'Befehlseingabe', + 'action.save': 'Ziel speichern', + 'action.cancel': 'Bearbeitung abbrechen', + 'action.pause': 'Ziel pausieren', + 'action.resume': 'Ziel fortsetzen', + 'action.edit': 'Ziel bearbeiten', + 'action.clear': 'Klares Ziel', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'phase.active': 'Текущая цель', + 'phase.active.disarmed': 'Неактивная цель', + 'phase.paused': 'Приостановленная цель', + 'phase.blocked': 'Заблокированная цель', + 'objective.aria': 'Цель Цель', + 'commandInput.aria': 'Ввод команды', + 'action.save': 'Сохранить цель', + 'action.cancel': 'Отменить редактирование', + 'action.pause': 'Приостановить цель', + 'action.resume': 'Возобновить цель', + 'action.edit': 'Изменить цель', + 'action.clear': 'Четкая цель', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'phase.active': 'Meta contínua', + 'phase.active.disarmed': 'Meta inativa', + 'phase.paused': 'Meta pausada', + 'phase.blocked': 'Meta bloqueada', + 'objective.aria': 'Objetivo da meta', + 'commandInput.aria': 'Entrada de comando', + 'action.save': 'Salvar meta', + 'action.cancel': 'Cancelar edição', + 'action.pause': 'Pausar meta', + 'action.resume': 'Retomar meta', + 'action.edit': 'Editar meta', + 'action.clear': 'Objetivo claro', +} satisfies Record diff --git a/packages/client/ui-input-trigger/locale/de.json b/packages/client/ui-input-trigger/locale/de.json new file mode 100644 index 0000000000..be4ceb8980 --- /dev/null +++ b/packages/client/ui-input-trigger/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Geben Sie Lenovo Services ein", + "description": "Erkennen Sie / und @ in der Eingabe, zeigen Sie die entsprechenden Kandidaten an und wählen Sie sie aus." + } +} diff --git a/packages/client/ui-input-trigger/locale/es.json b/packages/client/ui-input-trigger/locale/es.json new file mode 100644 index 0000000000..48be835ec5 --- /dev/null +++ b/packages/client/ui-input-trigger/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Ingrese a los servicios de Lenovo", + "description": "Reconocer / y @ en la entrada, mostrar y seleccionar los candidatos correspondientes." + } +} diff --git a/packages/client/ui-input-trigger/locale/fr.json b/packages/client/ui-input-trigger/locale/fr.json new file mode 100644 index 0000000000..225c4f9e3f --- /dev/null +++ b/packages/client/ui-input-trigger/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Entrez dans les services Lenovo", + "description": "Reconnaître / et @ dans la saisie, afficher et sélectionner les candidats correspondants." + } +} diff --git a/packages/client/ui-input-trigger/locale/pt.json b/packages/client/ui-input-trigger/locale/pt.json new file mode 100644 index 0000000000..91b25e4363 --- /dev/null +++ b/packages/client/ui-input-trigger/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Entre nos serviços Lenovo", + "description": "Reconheça / e @ na entrada, exiba e selecione os candidatos correspondentes." + } +} diff --git a/packages/client/ui-input-trigger/locale/ru.json b/packages/client/ui-input-trigger/locale/ru.json new file mode 100644 index 0000000000..9aeeefffd6 --- /dev/null +++ b/packages/client/ui-input-trigger/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Войдите в услуги Lenovo", + "description": "Распознайте / и @ во входных данных, отобразите и выберите соответствующих кандидатов." + } +} diff --git a/packages/client/ui-input-trigger/src/client/index.ts b/packages/client/ui-input-trigger/src/client/index.ts index cad11a5452..de4186135f 100644 --- a/packages/client/ui-input-trigger/src/client/index.ts +++ b/packages/client/ui-input-trigger/src/client/index.ts @@ -13,7 +13,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-session/client' import { InputTriggerService } from './service.ts' import type { MenuViewInjected } from './slots.ts' import { MenuView } from './MenuView.tsx' -import { en, ja, zh, type MenuKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type MenuKey } from './locales.ts' export { InputTriggerService } from './service.ts' export { InputTriggerController } from './controller.ts' @@ -58,7 +58,7 @@ export const inject = ['sessions', 'locale'] */ export function apply(ctx: ClientContext): void { ctx.plugin(InputTriggerService) - ctx.effect(() => ctx.locale.register(MENU_NS, { zh, en, ja }), 'ui-input-trigger: menu dictionaries') + ctx.effect(() => ctx.locale.register(MENU_NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-input-trigger: menu dictionaries') ctx.inject(['slots', 'inputTriggers', 'sessions'], (scope: ClientContext) => { const inputTriggers = scope.inputTriggers const sessions = scope.sessions diff --git a/packages/client/ui-input-trigger/src/client/locales.ts b/packages/client/ui-input-trigger/src/client/locales.ts index 1b11342393..63f0780d1d 100644 --- a/packages/client/ui-input-trigger/src/client/locales.ts +++ b/packages/client/ui-input-trigger/src/client/locales.ts @@ -45,3 +45,68 @@ export const ja = { 'crumbs.aria': 'フォルダナビゲーション', 'suggestions.aria': '候補を表示', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'command': 'Comandos', + 'skill': 'Habilidades', + 'subagent': 'Subagentes', + 'loading': 'Cargando…', + 'drill.aria': 'Explorar carpeta', + 'drill.hint': 'Explorar carpeta', + 'drill.key': 'Pestaña', + 'crumbs.aria': 'Navegación de carpetas', + 'suggestions.aria': 'Sugerencias de activación', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'command': 'Commandes', + 'skill': 'Compétences', + 'subagent': 'Sous-agents', + 'loading': 'Chargement…', + 'drill.aria': 'Parcourir le dossier', + 'drill.hint': 'Parcourir le dossier', + 'drill.key': 'Onglet', + 'crumbs.aria': 'Navigation dans les dossiers', + 'suggestions.aria': 'Suggestions de déclenchement', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'command': 'Befehle', + 'skill': 'Fähigkeiten', + 'subagent': 'Subagenten', + 'loading': 'Laden…', + 'drill.aria': 'Ordner durchsuchen', + 'drill.hint': 'Ordner durchsuchen', + 'drill.key': 'Tab', + 'crumbs.aria': 'Ordnernavigation', + 'suggestions.aria': 'Trigger-Vorschläge', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'command': 'Команды', + 'skill': 'Навыки', + 'subagent': 'Субагенты', + 'loading': 'Загрузка…', + 'drill.aria': 'Просмотреть папку', + 'drill.hint': 'Просмотреть папку', + 'drill.key': 'Вкладка', + 'crumbs.aria': 'Навигация по папкам', + 'suggestions.aria': 'Триггерные предложения', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'command': 'Comandos', + 'skill': 'Habilidades', + 'subagent': 'Subagentes', + 'loading': 'Carregando…', + 'drill.aria': 'Navegar na pasta', + 'drill.hint': 'Navegar na pasta', + 'drill.key': 'Guia', + 'crumbs.aria': 'Navegação de pastas', + 'suggestions.aria': 'Sugestões de gatilho', +} satisfies Record diff --git a/packages/client/ui-jobs/locale/de.json b/packages/client/ui-jobs/locale/de.json new file mode 100644 index 0000000000..87cc3c7d1f --- /dev/null +++ b/packages/client/ui-jobs/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Hintergrundaufgabenschnittstelle", + "description": "Zeigen Sie Hintergrundaufgabenlisten, Status und Echtzeitausgabe innerhalb einer Sitzung an." + } +} diff --git a/packages/client/ui-jobs/locale/es.json b/packages/client/ui-jobs/locale/es.json new file mode 100644 index 0000000000..9ce2933127 --- /dev/null +++ b/packages/client/ui-jobs/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de tareas en segundo plano", + "description": "Vea listas de tareas en segundo plano, estados y resultados en tiempo real dentro de una sesión." + } +} diff --git a/packages/client/ui-jobs/locale/fr.json b/packages/client/ui-jobs/locale/fr.json new file mode 100644 index 0000000000..e950919b81 --- /dev/null +++ b/packages/client/ui-jobs/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de tâche en arrière-plan", + "description": "Affichez les listes de tâches en arrière-plan, l'état et la sortie en temps réel au sein d'une session." + } +} diff --git a/packages/client/ui-jobs/locale/pt.json b/packages/client/ui-jobs/locale/pt.json new file mode 100644 index 0000000000..2820658f19 --- /dev/null +++ b/packages/client/ui-jobs/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de tarefa em segundo plano", + "description": "Visualize listas de tarefas em segundo plano, status e resultados em tempo real em uma sessão." + } +} diff --git a/packages/client/ui-jobs/locale/ru.json b/packages/client/ui-jobs/locale/ru.json new file mode 100644 index 0000000000..3d2a910429 --- /dev/null +++ b/packages/client/ui-jobs/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс фоновой задачи", + "description": "Просматривайте списки фоновых задач, статус и выходные данные в режиме реального времени в течение сеанса." + } +} diff --git a/packages/client/ui-jobs/src/client/index.ts b/packages/client/ui-jobs/src/client/index.ts index eb5f4d920c..5174aa1fb8 100644 --- a/packages/client/ui-jobs/src/client/index.ts +++ b/packages/client/ui-jobs/src/client/index.ts @@ -12,7 +12,7 @@ import type {} from '@deepseek-ai/dsh-api-job-controller/client' import type {} from '@deepseek-ai/dsh-client-locale/client' import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type {} from '@deepseek-ai/dsh-client-ui-session/client' -import { en, ja, NS, zh, type JobKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type JobKey } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { @@ -31,7 +31,7 @@ export const inject = ['jobs', 'slots', 'locale'] * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-jobs: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-jobs: dictionaries') ctx.slots.inject( 'conversation.session.header.actions', () => ctx.slots.register({ diff --git a/packages/client/ui-jobs/src/client/locales.ts b/packages/client/ui-jobs/src/client/locales.ts index c1aff15b1c..700009d9a5 100644 --- a/packages/client/ui-jobs/src/client/locales.ts +++ b/packages/client/ui-jobs/src/client/locales.ts @@ -137,3 +137,223 @@ export const ja = { 'terminal.expand': '残り {n} 行を表示', 'terminal.expandAria': '折りたたまれた出力 {n} 行を展開', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'count.live.one': 'Tareas en segundo plano en ejecución: {count}', + 'count.live.other': 'Tareas en segundo plano en ejecución: {count}', + 'count.idle.one': 'Tareas en segundo plano: {count}', + 'count.idle.other': 'Tareas en segundo plano: {count}', + 'list.aria': 'Trabajos en segundo plano', + 'section.live': 'corriendo', + 'section.settledCount': 'Terminado {count}', + 'section.clear': 'Borrar', + 'row.expandAria': 'Mostrar salida en vivo de {label}', + 'row.collapseAria': 'Ocultar salida en vivo de {label}', + 'kill.stop': 'Detener tarea {label}', + 'kill.confirm': 'Haga clic nuevamente para confirmar', + 'kill.confirmAction': 'Confirmar parada', + 'kill.failed': 'Detener falló', + 'status.running': 'corriendo', + 'status.stopping': 'parando', + 'status.completed': 'completado', + 'status.killed': 'cancelado', + 'status.failed': 'falló', + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m', + 'duration.title.live': 'Corriendo para {duration}', + 'duration.title.done': 'Tomó {duration}', + 'output.gap': '… la producción anterior cayó…', + 'output.error': 'transmisión de salida en vivo interrumpida: {error}', + 'terminal.signal': 'señal {signal}', + 'terminal.exitCode': 'salir {code}', + 'terminal.noExitCode': 'sin código de salida', + 'terminal.commandLine': 'Línea de comando {n}', + 'terminal.running': 'corriendo', + 'terminal.failed': 'falló', + 'terminal.done': 'hecho', + 'terminal.copy': 'Copiar', + 'terminal.copied': 'Copiado', + 'terminal.noOutput': '(sin salida)', + 'terminal.collapse': 'Contraer', + 'terminal.collapseAria': 'Contraer salida', + 'terminal.expand': 'Mostrar {n} más líneas', + 'terminal.expandAria': 'Expandir líneas de salida colapsadas {n}', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'count.live.one': 'Tâches en arrière-plan en cours : {count}', + 'count.live.other': 'Tâches en arrière-plan en cours : {count}', + 'count.idle.one': 'Tâches en arrière-plan : {count}', + 'count.idle.other': 'Tâches en arrière-plan : {count}', + 'list.aria': 'Travaux en arrière-plan', + 'section.live': 'Courir', + 'section.settledCount': 'Terminé {count}', + 'section.clear': 'Effacer', + 'row.expandAria': 'Afficher la sortie en direct de {label}', + 'row.collapseAria': 'Masquer la sortie en direct de {label}', + 'kill.stop': 'Arrêter la tâche {label}', + 'kill.confirm': 'Cliquez à nouveau pour confirmer', + 'kill.confirmAction': "Confirmer l'arrêt", + 'kill.failed': "L'arrêt a échoué", + 'status.running': 'courir', + 'status.stopping': 'arrêter', + 'status.completed': 'terminé', + 'status.killed': 'annulé', + 'status.failed': 'échoué', + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m', + 'duration.title.live': 'Courir pour {duration}', + 'duration.title.done': 'A pris {duration}', + 'output.gap': '… la production antérieure a chuté…', + 'output.error': 'flux de sortie en direct interrompu : {error}', + 'terminal.signal': 'signal {signal}', + 'terminal.exitCode': 'quitter {code}', + 'terminal.noExitCode': 'pas de code de sortie', + 'terminal.commandLine': 'Ligne de commande {n}', + 'terminal.running': 'courir', + 'terminal.failed': 'échoué', + 'terminal.done': 'fait', + 'terminal.copy': 'Copier', + 'terminal.copied': 'Copié', + 'terminal.noOutput': '(pas de sortie)', + 'terminal.collapse': 'Réduire', + 'terminal.collapseAria': 'Réduire la sortie', + 'terminal.expand': 'Afficher {n} plus de lignes', + 'terminal.expandAria': 'Développer les lignes de sortie réduites {n}', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'count.live.one': 'Laufende Hintergrundaufgaben: {count}', + 'count.live.other': 'Laufende Hintergrundaufgaben: {count}', + 'count.idle.one': 'Hintergrundaufgaben: {count}', + 'count.idle.other': 'Hintergrundaufgaben: {count}', + 'list.aria': 'Hintergrundjobs', + 'section.live': 'Laufen', + 'section.settledCount': 'Fertig {count}', + 'section.clear': 'Klar', + 'row.expandAria': 'Live-Ausgabe von {label} anzeigen', + 'row.collapseAria': 'Live-Ausgabe von {label} ausblenden', + 'kill.stop': 'Stoppen Sie die Aufgabe {label}', + 'kill.confirm': 'Klicken Sie zur Bestätigung erneut', + 'kill.confirmAction': 'Stopp bestätigen', + 'kill.failed': 'Stoppen ist fehlgeschlagen', + 'status.running': 'laufen', + 'status.stopping': 'anhalten', + 'status.completed': 'abgeschlossen', + 'status.killed': 'abgesagt', + 'status.failed': 'gescheitert', + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m', + 'duration.title.live': 'Laufen für {duration}', + 'duration.title.done': 'Nahm {duration}', + 'output.gap': '… frühere Produktion gesunken …', + 'output.error': 'Live-Ausgabestream unterbrochen: {error}', + 'terminal.signal': 'Signal {signal}', + 'terminal.exitCode': 'Beenden Sie {code}', + 'terminal.noExitCode': 'kein Exit-Code', + 'terminal.commandLine': 'Befehlszeile {n}', + 'terminal.running': 'laufen', + 'terminal.failed': 'gescheitert', + 'terminal.done': 'erledigt', + 'terminal.copy': 'Kopieren', + 'terminal.copied': 'Kopiert', + 'terminal.noOutput': '(keine Ausgabe)', + 'terminal.collapse': 'Einklappen', + 'terminal.collapseAria': 'Ausgabe reduzieren', + 'terminal.expand': 'Zeige {n} weitere Zeilen', + 'terminal.expandAria': 'Erweitern Sie die minimierten Ausgabezeilen {n}', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'count.live.one': 'Фоновые задачи выполняются: {count}', + 'count.live.other': 'Фоновые задачи выполняются: {count}', + 'count.idle.one': 'Фоновые задачи: {count}', + 'count.idle.other': 'Фоновые задачи: {count}', + 'list.aria': 'Фоновые задания', + 'section.live': 'Бег', + 'section.settledCount': 'Закончено {count}', + 'section.clear': 'Очистить', + 'row.expandAria': 'Показать живой вывод {label}', + 'row.collapseAria': 'Скрыть прямой вывод {label}', + 'kill.stop': 'Остановить задачу {label}', + 'kill.confirm': 'Нажмите еще раз, чтобы подтвердить', + 'kill.confirmAction': 'Подтвердить остановку', + 'kill.failed': 'Остановить не удалось', + 'status.running': 'бег', + 'status.stopping': 'остановка', + 'status.completed': 'завершено', + 'status.killed': 'отменен', + 'status.failed': 'не удалось', + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m', + 'duration.title.live': 'Бегу за {duration}', + 'duration.title.done': 'Взял {duration}', + 'output.gap': '… ранее объемы производства упали …', + 'output.error': 'поток вывода в реальном времени прерван: {error}', + 'terminal.signal': 'сигнал {signal}', + 'terminal.exitCode': 'выход {code}', + 'terminal.noExitCode': 'нет кода выхода', + 'terminal.commandLine': 'Командная строка {n}', + 'terminal.running': 'бег', + 'terminal.failed': 'не удалось', + 'terminal.done': 'сделано', + 'terminal.copy': 'Копировать', + 'terminal.copied': 'Скопировано', + 'terminal.noOutput': '(нет вывода)', + 'terminal.collapse': 'Свернуть', + 'terminal.collapseAria': 'Свернуть вывод', + 'terminal.expand': 'Показать {n} больше строк', + 'terminal.expandAria': 'Развернуть свернутые выходные строки {n}', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'count.live.one': 'Tarefas em segundo plano em execução: {count}', + 'count.live.other': 'Tarefas em segundo plano em execução: {count}', + 'count.idle.one': 'Tarefas em segundo plano: {count}', + 'count.idle.other': 'Tarefas em segundo plano: {count}', + 'list.aria': 'Trabalhos em segundo plano', + 'section.live': 'Correndo', + 'section.settledCount': 'Concluído {count}', + 'section.clear': 'Limpar', + 'row.expandAria': 'Mostrar saída ao vivo de {label}', + 'row.collapseAria': 'Ocultar saída ao vivo de {label}', + 'kill.stop': 'Parar tarefa {label}', + 'kill.confirm': 'Clique novamente para confirmar', + 'kill.confirmAction': 'Confirmar parada', + 'kill.failed': 'Falha na parada', + 'status.running': 'correndo', + 'status.stopping': 'parando', + 'status.completed': 'concluído', + 'status.killed': 'cancelado', + 'status.failed': 'falhou', + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m', + 'duration.title.live': 'Correndo por {duration}', + 'duration.title.done': 'Levou {duration}', + 'output.gap': '… a produção anterior caiu…', + 'output.error': 'transmissão ao vivo interrompida: {error}', + 'terminal.signal': 'sinal {signal}', + 'terminal.exitCode': 'sair {code}', + 'terminal.noExitCode': 'sem código de saída', + 'terminal.commandLine': 'Linha de comando {n}', + 'terminal.running': 'correndo', + 'terminal.failed': 'falhou', + 'terminal.done': 'feito', + 'terminal.copy': 'Copiar', + 'terminal.copied': 'Copiado', + 'terminal.noOutput': '(sem saída)', + 'terminal.collapse': 'Recolher', + 'terminal.collapseAria': 'Recolher saída', + 'terminal.expand': 'Mostrar mais {n} linhas', + 'terminal.expandAria': 'Expanda {n} linhas de saída recolhidas', +} satisfies Record diff --git a/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx b/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx index e6866c4182..4dcf892f87 100644 --- a/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx +++ b/packages/client/ui-jobs/tests/job-list-action.client.spec.tsx @@ -5,7 +5,7 @@ import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' import type { JobsSnapshot, JobView, ObservedJob } from '@deepseek-ai/dsh-api-job-controller/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' import { JobListAction, type JobListActionProps } from '../src/client/JobListAction.tsx' -import { zh } from '../src/client/locales.ts' +import { zh, ru } from '../src/client/locales.ts' afterEach(() => { cleanup() @@ -590,3 +590,12 @@ describe('JobListAction human kill', () => { expect(document.querySelector('[data-kill-state]')).toBeNull() }) }) + +it.each([1, 2, 5, 21])('renders count-independent Russian labels for %i jobs', (count) => { + const jobs = Array.from({ length: count }, (_, i) => job({ id: `job-${i}` as JobView['id'] })) + const translate: JobListActionProps['t'] = makeTranslate(ru) + const view = render() + expect(screen.getByRole('button', { name: `Фоновые задачи выполняются: ${count}` })).toBeDefined() + view.rerender( ({ ...item, status: 'completed', finishedAt: 1_700_000_003_000 })))} t={translate} />) + expect(screen.getByRole('button', { name: `Фоновые задачи: ${count}` })).toBeDefined() +}) diff --git a/packages/client/ui-layout/locale/de.json b/packages/client/ui-layout/locale/de.json new file mode 100644 index 0000000000..9d6d23789f --- /dev/null +++ b/packages/client/ui-layout/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Layout anwenden", + "description": "Organisieren Sie Navigation, Konversationen und Seitenbereiche und unterstützen Sie das Ziehen, um das Layout anzupassen." + } +} diff --git a/packages/client/ui-layout/locale/es.json b/packages/client/ui-layout/locale/es.json new file mode 100644 index 0000000000..01d553183d --- /dev/null +++ b/packages/client/ui-layout/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Aplicar diseño", + "description": "Organice la navegación, las conversaciones y los paneles laterales, y admita arrastrar para ajustar el diseño." + } +} diff --git a/packages/client/ui-layout/locale/fr.json b/packages/client/ui-layout/locale/fr.json new file mode 100644 index 0000000000..7526ace8cc --- /dev/null +++ b/packages/client/ui-layout/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Appliquer la mise en page", + "description": "Organisez la navigation, les conversations et les panneaux latéraux, et prenez en charge le glisser-déposer pour ajuster la mise en page." + } +} diff --git a/packages/client/ui-layout/locale/pt.json b/packages/client/ui-layout/locale/pt.json new file mode 100644 index 0000000000..10b0dcaae8 --- /dev/null +++ b/packages/client/ui-layout/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Aplicar layout", + "description": "Organize a navegação, as conversas e os painéis laterais e suporte o recurso de arrastar para ajustar o layout." + } +} diff --git a/packages/client/ui-layout/locale/ru.json b/packages/client/ui-layout/locale/ru.json new file mode 100644 index 0000000000..ab90630f4d --- /dev/null +++ b/packages/client/ui-layout/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Применить макет", + "description": "Организуйте навигацию, диалоги и боковые панели, а также поддерживайте перетаскивание для настройки макета." + } +} diff --git a/packages/client/ui-layout/src/client/index.ts b/packages/client/ui-layout/src/client/index.ts index fb66376cfb..ee9da22d2b 100644 --- a/packages/client/ui-layout/src/client/index.ts +++ b/packages/client/ui-layout/src/client/index.ts @@ -18,7 +18,7 @@ import { AppFrame } from './AppFrame.tsx' import { createLayoutStore } from './stores.ts' import { LayoutController } from './service.ts' import type { ShortcutCommandId } from '@deepseek-ai/dsh-client-shortcuts/client' -import { en, ja, zh } from './shortcut-locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './shortcut-locales.ts' import { ThemePresenter } from './theme-presenter.ts' // Contract exports only (export-convergence rule: cross-package consumers @@ -156,7 +156,7 @@ export const inject = ['slots', 'theme', 'locale', 'shortcuts'] * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register('shortcuts.layout', { zh, en, ja }), 'layout: command labels') + ctx.effect(() => ctx.locale.register('shortcuts.layout', { zh, en, ja, es, fr, de, ru, pt }), 'layout: command labels') const t = ctx.locale.bind('shortcuts.layout') ctx.effect(() => { diff --git a/packages/client/ui-layout/src/client/shortcut-locales.ts b/packages/client/ui-layout/src/client/shortcut-locales.ts index dcccfc8641..c87cdc5026 100644 --- a/packages/client/ui-layout/src/client/shortcut-locales.ts +++ b/packages/client/ui-layout/src/client/shortcut-locales.ts @@ -7,3 +7,18 @@ export const en: Record = { toggle: 'Toggle left sideba export const ja = { 'toggle': '左サイドバーを切り替え', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { toggle: 'Alternar barra lateral izquierda' } satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { toggle: 'Basculer la barre latérale gauche' } satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { toggle: 'Linke Seitenleiste umschalten' } satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { toggle: 'Переключить левую боковую панель' } satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { toggle: 'Alternar barra lateral esquerda' } satisfies Record diff --git a/packages/client/ui-message-feedback/locale/de.json b/packages/client/ui-message-feedback/locale/de.json new file mode 100644 index 0000000000..71b5fc4559 --- /dev/null +++ b/packages/client/ui-message-feedback/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Nachrichten-Feedback-Schnittstelle", + "description": "Stellen Sie „Gefällt mir“- und „Gefällt mir nicht“-Schaltflächen für Antworten und Popup-Fenster für Gesprächsfeedback bereit." + } +} diff --git a/packages/client/ui-message-feedback/locale/es.json b/packages/client/ui-message-feedback/locale/es.json new file mode 100644 index 0000000000..26672a96ab --- /dev/null +++ b/packages/client/ui-message-feedback/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de comentarios de mensajes", + "description": "Proporcione botones Me gusta y No me gusta para las ventanas emergentes de respuestas y comentarios de la conversación." + } +} diff --git a/packages/client/ui-message-feedback/locale/fr.json b/packages/client/ui-message-feedback/locale/fr.json new file mode 100644 index 0000000000..71009572ad --- /dev/null +++ b/packages/client/ui-message-feedback/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de retour de messages", + "description": "Fournissez des boutons J'aime et Je n'aime pas pour les réponses et les fenêtres contextuelles de commentaires sur la conversation." + } +} diff --git a/packages/client/ui-message-feedback/locale/pt.json b/packages/client/ui-message-feedback/locale/pt.json new file mode 100644 index 0000000000..dcb779d6b0 --- /dev/null +++ b/packages/client/ui-message-feedback/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de feedback de mensagens", + "description": "Forneça botões de gostar e não gostar para respostas e janelas pop-up de feedback de conversas." + } +} diff --git a/packages/client/ui-message-feedback/locale/ru.json b/packages/client/ui-message-feedback/locale/ru.json new file mode 100644 index 0000000000..388f33bf3a --- /dev/null +++ b/packages/client/ui-message-feedback/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс обратной связи по сообщениям", + "description": "Предоставьте кнопки «Мне нравится» и «Не нравится» для ответов и всплывающих окон обратной связи." + } +} diff --git a/packages/client/ui-message-feedback/src/client/index.ts b/packages/client/ui-message-feedback/src/client/index.ts index 7d95fe469a..0d22230e13 100644 --- a/packages/client/ui-message-feedback/src/client/index.ts +++ b/packages/client/ui-message-feedback/src/client/index.ts @@ -26,7 +26,7 @@ import { FeedbackDialog } from './FeedbackDialog.tsx' import { MessageFeedbackActions } from './MessageFeedbackActions.tsx' import type { FeedbackDialogInjected, MessageFeedbackInjected } from './slots.ts' import { FeedbackSurface } from './surface.ts' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' export type { MessageFeedbackActionFailure, MessageFeedbackActionResult, MessageFeedbackStatus, @@ -66,7 +66,7 @@ export const inject = ['slots', 'remote', 'remote.messageFeedback', 'remote.sess * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-message-feedback: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-message-feedback: dictionaries') const surfaces = new Map() const surfaceFor = (sessionId: SessionId): FeedbackSurface => { diff --git a/packages/client/ui-message-feedback/src/client/locales.ts b/packages/client/ui-message-feedback/src/client/locales.ts index f55054c71d..d5a0fc66e3 100644 --- a/packages/client/ui-message-feedback/src/client/locales.ts +++ b/packages/client/ui-message-feedback/src/client/locales.ts @@ -81,3 +81,123 @@ export const ja = { 'error.generic': 'フィードバックを保存できませんでした', 'error.noteTooLarge': '説明が長すぎます。短くして再送信してください', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'action.like': 'buena respuesta', + 'action.likeActive': 'Eliminar calificación', + 'action.dislike': 'Mala respuesta', + 'action.dislikeActive': 'Eliminar calificación', + 'dialog.title': 'Enviar comentarios', + 'dialog.categories': 'Categoría de comentarios', + 'dialog.detail': 'Detalles de comentarios', + 'dialog.hint': 'Añade detalles para ayudarnos a mejorar. Su envío incluirá el registro de conversación actual.', + 'category.task-result': 'Resultado de la tarea', + 'category.instruction-following': 'Comprensión y seguimiento de instrucciones.', + 'category.product-interaction': 'Características del producto e interacción.', + 'category.service-stability': 'Estabilidad y velocidad', + 'category.resource-cost': 'Uso y costo de recursos', + 'category.security-privacy-permission': 'Seguridad, privacidad y permisos', + 'category.other': 'Otro', + 'toast.recorded': 'Gracias por tus comentarios', + 'error.conflict': 'Esta retroalimentación cambió en otros lugares; se muestra el último estado', + 'error.load': 'No se pudieron cargar los comentarios', + 'error.generic': 'No se pudieron guardar los comentarios', + 'error.noteTooLarge': 'La descripción es demasiado larga; acortarlo y enviarlo de nuevo', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'action.like': 'Bonne réponse', + 'action.likeActive': 'Supprimer la note', + 'action.dislike': 'Mauvaise réponse', + 'action.dislikeActive': 'Supprimer la note', + 'dialog.title': 'Soumettre des commentaires', + 'dialog.categories': 'Catégorie de commentaires', + 'dialog.detail': 'Détails des commentaires', + 'dialog.hint': 'Ajoutez des détails pour nous aider à nous améliorer. Votre soumission inclura le journal de conversation actuel.', + 'category.task-result': 'Résultat de la tâche', + 'category.instruction-following': 'Compréhension et suivi des instructions', + 'category.product-interaction': 'Caractéristiques du produit et interaction', + 'category.service-stability': 'Stabilité et vitesse', + 'category.resource-cost': 'Utilisation et coût des ressources', + 'category.security-privacy-permission': 'Sécurité, confidentialité et autorisations', + 'category.other': 'Autre', + 'toast.recorded': 'Merci pour vos commentaires', + 'error.conflict': 'Cette rétroaction a changé ailleurs ; le dernier état est affiché', + 'error.load': 'Impossible de charger les commentaires', + 'error.generic': "Impossible d'enregistrer les commentaires", + 'error.noteTooLarge': 'La description est trop longue ; raccourcissez-le et soumettez-le à nouveau', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'action.like': 'Gute Resonanz', + 'action.likeActive': 'Bewertung entfernen', + 'action.dislike': 'Schlechte Reaktion', + 'action.dislikeActive': 'Bewertung entfernen', + 'dialog.title': 'Feedback abgeben', + 'dialog.categories': 'Feedback-Kategorie', + 'dialog.detail': 'Feedback-Details', + 'dialog.hint': 'Fügen Sie Details hinzu, um uns bei der Verbesserung zu helfen. Ihre Einreichung enthält das aktuelle Gesprächsprotokoll.', + 'category.task-result': 'Aufgabenergebnis', + 'category.instruction-following': 'Anweisungen verstehen und befolgen', + 'category.product-interaction': 'Produkteigenschaften und Interaktion', + 'category.service-stability': 'Stabilität und Geschwindigkeit', + 'category.resource-cost': 'Ressourcennutzung und Kosten', + 'category.security-privacy-permission': 'Sicherheit, Datenschutz und Berechtigungen', + 'category.other': 'Andere', + 'toast.recorded': 'Vielen Dank für Ihr Feedback', + 'error.conflict': 'Dieses Feedback hat sich an anderer Stelle geändert; der aktuelle Stand wird angezeigt', + 'error.load': 'Feedback konnte nicht geladen werden', + 'error.generic': 'Das Feedback konnte nicht gespeichert werden', + 'error.noteTooLarge': 'Die Beschreibung ist zu lang; kürzen Sie es und reichen Sie es erneut ein', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'action.like': 'Хороший ответ', + 'action.likeActive': 'Удалить оценку', + 'action.dislike': 'Плохой ответ', + 'action.dislikeActive': 'Удалить оценку', + 'dialog.title': 'Отправить отзыв', + 'dialog.categories': 'Категория обратной связи', + 'dialog.detail': 'Подробности обратной связи', + 'dialog.hint': 'Добавьте детали, чтобы помочь нам стать лучше. Ваше сообщение будет включать текущий журнал разговоров.', + 'category.task-result': 'Результат задачи', + 'category.instruction-following': 'Понимание и следование инструкциям', + 'category.product-interaction': 'Особенности продукта и взаимодействие', + 'category.service-stability': 'Стабильность и скорость', + 'category.resource-cost': 'Использование ресурсов и стоимость', + 'category.security-privacy-permission': 'Безопасность, конфиденциальность и разрешения', + 'category.other': 'Другое', + 'toast.recorded': 'Спасибо за ваш отзыв', + 'error.conflict': 'Этот отзыв изменился в другом месте; отображается последнее состояние', + 'error.load': 'Не удалось загрузить отзыв.', + 'error.generic': 'Не удалось сохранить отзыв', + 'error.noteTooLarge': 'Описание слишком длинное; сократите его и отправьте снова', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'action.like': 'Boa resposta', + 'action.likeActive': 'Remover avaliação', + 'action.dislike': 'Resposta ruim', + 'action.dislikeActive': 'Remover avaliação', + 'dialog.title': 'Enviar comentários', + 'dialog.categories': 'Categoria de comentários', + 'dialog.detail': 'Detalhes do feedback', + 'dialog.hint': 'Adicione detalhes para nos ajudar a melhorar. Seu envio incluirá o registro da conversa atual.', + 'category.task-result': 'Resultado da tarefa', + 'category.instruction-following': 'Compreensão e seguimento das instruções', + 'category.product-interaction': 'Recursos e interação do produto', + 'category.service-stability': 'Estabilidade e velocidade', + 'category.resource-cost': 'Uso e custo de recursos', + 'category.security-privacy-permission': 'Segurança, privacidade e permissões', + 'category.other': 'Outro', + 'toast.recorded': 'Obrigado pelo seu feedback', + 'error.conflict': 'Este feedback mudou em outro lugar; o estado mais recente é mostrado', + 'error.load': 'Não foi possível carregar o feedback', + 'error.generic': 'Não foi possível salvar o feedback', + 'error.noteTooLarge': 'A descrição é muito longa; encurte-o e envie novamente', +} satisfies Record diff --git a/packages/client/ui-model-selection/locale/de.json b/packages/client/ui-model-selection/locale/de.json new file mode 100644 index 0000000000..c37cd4ec24 --- /dev/null +++ b/packages/client/ui-model-selection/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zur Modellauswahl", + "description": "Sehen Sie sich die verfügbaren Modelle an und wechseln Sie das von der aktuellen Sitzung verwendete Modell." + } +} diff --git a/packages/client/ui-model-selection/locale/es.json b/packages/client/ui-model-selection/locale/es.json new file mode 100644 index 0000000000..4ce95350a1 --- /dev/null +++ b/packages/client/ui-model-selection/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de selección de modelo", + "description": "Vea los modelos disponibles y cambie el modelo utilizado por la sesión actual." + } +} diff --git a/packages/client/ui-model-selection/locale/fr.json b/packages/client/ui-model-selection/locale/fr.json new file mode 100644 index 0000000000..bc3548d52e --- /dev/null +++ b/packages/client/ui-model-selection/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de sélection de modèle", + "description": "Affichez les modèles disponibles et changez le modèle utilisé par la session en cours." + } +} diff --git a/packages/client/ui-model-selection/locale/pt.json b/packages/client/ui-model-selection/locale/pt.json new file mode 100644 index 0000000000..17caef7c92 --- /dev/null +++ b/packages/client/ui-model-selection/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de seleção de modelo", + "description": "Visualize os modelos disponíveis e alterne o modelo usado pela sessão atual." + } +} diff --git a/packages/client/ui-model-selection/locale/ru.json b/packages/client/ui-model-selection/locale/ru.json new file mode 100644 index 0000000000..572c9b5541 --- /dev/null +++ b/packages/client/ui-model-selection/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс выбора модели", + "description": "Просмотрите доступные модели и переключите модель, используемую в текущем сеансе." + } +} diff --git a/packages/client/ui-model-selection/src/client/index.ts b/packages/client/ui-model-selection/src/client/index.ts index ae47f156ed..f1f15a1154 100644 --- a/packages/client/ui-model-selection/src/client/index.ts +++ b/packages/client/ui-model-selection/src/client/index.ts @@ -27,7 +27,7 @@ import type { ModelDirectoryState } from './directory.ts' import { ModelDirectoryResolver } from './service.ts' import type { ModelSelectInjected } from './slots.ts' import { ModelSelect } from './ModelSelect.tsx' -import { en, ja, zh, type ModelKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type ModelKey } from './locales.ts' import { orderModelProviders } from './provider-order.ts' export { ModelDirectory } from './directory.ts' @@ -113,7 +113,7 @@ export const inject = ['commandUi', 'locale', 'sessions', 'slots', 'remote', 're * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-model-selection: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-model-selection: dictionaries') // Non-slot faces (the command description, the popup option builder) read // through the bound translate; the seat component reads the standard seat. diff --git a/packages/client/ui-model-selection/src/client/locales.ts b/packages/client/ui-model-selection/src/client/locales.ts index 4e931226ea..cb2749f9a9 100644 --- a/packages/client/ui-model-selection/src/client/locales.ts +++ b/packages/client/ui-model-selection/src/client/locales.ts @@ -91,3 +91,138 @@ export const ja = { 'empty.models': '利用可能なモデルがありません。', 'empty.efforts': 'このモデルには推論強度の設定がありません。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'provider.account': 'Cuenta DeepSeek', + 'command.label': 'modelo', + 'command.description': 'Selecciona el modelo para esta conversación', + 'option.loadError': 'No se pudo cargar el catálogo: {message}', + 'trigger.fallback': 'Seleccionar modelo', + 'trigger.loading': 'Cargando modelos…', + 'trigger.selectAria': 'Seleccionar modelo', + 'trigger.aria': 'Seleccionar modelo, actual {model}', + 'trigger.ariaEffort': 'Seleccionar modelo, {model} actual, esfuerzo de razonamiento {effort}', + 'menu.aria': 'Esfuerzo de modelo y razonamiento.', + 'menu.model': 'modelo', + 'menu.effort': 'Esfuerzo', + 'effort.providerDefault': 'Predeterminado', + 'status.loading': 'Lista de modelos refrescante...', + 'error.action': 'Error en la operación del modelo: {message}', + 'error.sessionInUse': 'Esta sesión ya está en uso, posiblemente por otra instancia de DSH en ejecución (como la web dsh o la aplicación de escritorio). Salga de otras instancias de DSH en ejecución y vuelva a intentarlo.', + 'action.reload': 'recargar', + 'warning.groupLoad': '{name} no pudo cargar: {message}', + 'search.placeholder': 'Buscar modelos…', + 'search.clear': 'Borrar búsqueda', + 'search.empty': 'No hay modelos coincidentes.', + 'empty.models': 'No hay modelos disponibles.', + 'empty.efforts': 'Este modelo no proporciona niveles de esfuerzo de razonamiento.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'provider.account': 'Compte DeepSeek', + 'command.label': 'Modèle', + 'command.description': 'Sélectionnez le modèle pour cette conversation', + 'option.loadError': 'Échec du chargement du catalogue : {message}', + 'trigger.fallback': 'Sélectionnez le modèle', + 'trigger.loading': 'Chargement des modèles…', + 'trigger.selectAria': 'Sélectionnez le modèle', + 'trigger.aria': 'Sélectionnez le modèle, {model} actuel', + 'trigger.ariaEffort': 'Sélectionnez le modèle, {model} actuel, effort de raisonnement {effort}', + 'menu.aria': 'Effort de modélisation et de raisonnement', + 'menu.model': 'Modèle', + 'menu.effort': 'Effort', + 'effort.providerDefault': 'Par défaut', + 'status.loading': 'Liste des modèles actualisée…', + 'error.action': "Échec de l'opération du modèle : {message}", + 'error.sessionInUse': "Cette session est déjà utilisée, éventuellement par une autre instance DSH en cours d'exécution (telle que le Web dsh ou l'application de bureau). Quittez les autres instances DSH en cours d'exécution et réessayez.", + 'action.reload': 'Recharger', + 'warning.groupLoad': 'Échec du chargement de {name} : {message}', + 'search.placeholder': 'Rechercher des modèles…', + 'search.clear': 'Effacer la recherche', + 'search.empty': 'Aucun modèle correspondant.', + 'empty.models': 'Aucun modèle disponible.', + 'empty.efforts': 'Ce modèle ne fournit aucun niveau d’effort de raisonnement.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'provider.account': 'DeepSeek-Konto', + 'command.label': 'Modell', + 'command.description': 'Wählen Sie das Modell für dieses Gespräch aus', + 'option.loadError': 'Katalog konnte nicht geladen werden: {message}', + 'trigger.fallback': 'Modell auswählen', + 'trigger.loading': 'Modelle werden geladen…', + 'trigger.selectAria': 'Modell auswählen', + 'trigger.aria': 'Modell auswählen, aktuell {model}', + 'trigger.ariaEffort': 'Modell auswählen, aktuell {model}, Argumentationsaufwand {effort}', + 'menu.aria': 'Modell- und Argumentationsaufwand', + 'menu.model': 'Modell', + 'menu.effort': 'Aufwand', + 'effort.providerDefault': 'Standard', + 'status.loading': 'Erfrischende Modellliste…', + 'error.action': 'Modellvorgang fehlgeschlagen: {message}', + 'error.sessionInUse': 'Diese Sitzung wird bereits verwendet, möglicherweise von einer anderen laufenden DSH-Instanz (z. B. dsh Web oder der Desktop-App). Beenden Sie andere laufende DSH-Instanzen und versuchen Sie es erneut.', + 'action.reload': 'Neu laden', + 'warning.groupLoad': '{name} konnte nicht geladen werden: {message}', + 'search.placeholder': 'Modelle suchen…', + 'search.clear': 'Suche löschen', + 'search.empty': 'Keine passenden Modelle.', + 'empty.models': 'Keine Modelle verfügbar.', + 'empty.efforts': 'Dieses Modell liefert keine Argumentationsaufwandsstufen.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'provider.account': 'DeepSeek Аккаунт', + 'command.label': 'Модель', + 'command.description': 'Выберите модель для этого разговора', + 'option.loadError': 'Не удалось загрузить каталог: {message}.', + 'trigger.fallback': 'Выберите модель', + 'trigger.loading': 'Загрузка моделей…', + 'trigger.selectAria': 'Выберите модель', + 'trigger.aria': 'Выберите модель, текущую {model}', + 'trigger.ariaEffort': 'Выберите модель, текущую версию {model}, аргументацию {effort}', + 'menu.aria': 'Модель и рассуждения', + 'menu.model': 'Модель', + 'menu.effort': 'Усилие', + 'effort.providerDefault': 'По умолчанию', + 'status.loading': 'Обновление списка моделей…', + 'error.action': 'Не удалось выполнить операцию модели: {message}.', + 'error.sessionInUse': 'Этот сеанс уже используется, возможно, другим работающим экземпляром DSH (например, веб-сайтом dsh или настольным приложением). Закройте другие работающие экземпляры DSH и повторите попытку.', + 'action.reload': 'Перезагрузить', + 'warning.groupLoad': '{name} не удалось загрузить: {message}', + 'search.placeholder': 'Поиск моделей…', + 'search.clear': 'Очистить поиск', + 'search.empty': 'Нет подходящих моделей.', + 'empty.models': 'Нет доступных моделей.', + 'empty.efforts': 'Эта модель не обеспечивает обоснования уровней усилий.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'provider.account': 'DeepSeek Conta', + 'command.label': 'Modelo', + 'command.description': 'Selecione o modelo para esta conversa', + 'option.loadError': 'Falha ao carregar o catálogo: {message}', + 'trigger.fallback': 'Selecione o modelo', + 'trigger.loading': 'Carregando modelos…', + 'trigger.selectAria': 'Selecione o modelo', + 'trigger.aria': 'Selecione o modelo, atual {model}', + 'trigger.ariaEffort': 'Selecione modelo, atual {model}, esforço de raciocínio {effort}', + 'menu.aria': 'Modelo e esforço de raciocínio', + 'menu.model': 'Modelo', + 'menu.effort': 'Esforço', + 'effort.providerDefault': 'Padrão', + 'status.loading': 'Atualizando lista de modelos…', + 'error.action': 'Falha na operação do modelo: {message}', + 'error.sessionInUse': 'Esta sessão já está em uso, possivelmente por outra instância DSH em execução (como dsh web ou aplicativo de desktop). Saia de outras instâncias DSH em execução e tente novamente.', + 'action.reload': 'Recarregar', + 'warning.groupLoad': '{name} falhou ao carregar: {message}', + 'search.placeholder': 'Pesquisar modelos…', + 'search.clear': 'Limpar pesquisa', + 'search.empty': 'Não há modelos correspondentes.', + 'empty.models': 'Nenhum modelo disponível.', + 'empty.efforts': 'Este modelo não fornece níveis de esforço de raciocínio.', +} satisfies Record diff --git a/packages/client/ui-open-in-app/locale/de.json b/packages/client/ui-open-in-app/locale/de.json new file mode 100644 index 0000000000..f5b24225cb --- /dev/null +++ b/packages/client/ui-open-in-app/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zum Öffnen externer Anwendungen", + "description": "Bietet den Eintrag „Öffnen mit“ in der Sitzungs- und Dokumentvorschau." + } +} diff --git a/packages/client/ui-open-in-app/locale/es.json b/packages/client/ui-open-in-app/locale/es.json new file mode 100644 index 0000000000..1164cb3c6b --- /dev/null +++ b/packages/client/ui-open-in-app/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de apertura de aplicaciones externas", + "description": "Proporciona entrada \"Abrir con\" en la sesión y vista previa del documento." + } +} diff --git a/packages/client/ui-open-in-app/locale/fr.json b/packages/client/ui-open-in-app/locale/fr.json new file mode 100644 index 0000000000..5f104625e3 --- /dev/null +++ b/packages/client/ui-open-in-app/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface d'ouverture d'application externe", + "description": "Fournit une entrée « Ouvrir avec » dans la session et un aperçu du document." + } +} diff --git a/packages/client/ui-open-in-app/locale/pt.json b/packages/client/ui-open-in-app/locale/pt.json new file mode 100644 index 0000000000..105759e7e0 --- /dev/null +++ b/packages/client/ui-open-in-app/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de abertura de aplicativo externo", + "description": "Fornece entrada \"Abrir com\" na sessão e visualização do documento." + } +} diff --git a/packages/client/ui-open-in-app/locale/ru.json b/packages/client/ui-open-in-app/locale/ru.json new file mode 100644 index 0000000000..2655015ac7 --- /dev/null +++ b/packages/client/ui-open-in-app/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс открытия внешнего приложения", + "description": "Обеспечивает запись «Открыть с помощью» в сеансе и предварительном просмотре документа." + } +} diff --git a/packages/client/ui-open-in-app/src/client/index.ts b/packages/client/ui-open-in-app/src/client/index.ts index db99a2a23e..8da9f15f9e 100644 --- a/packages/client/ui-open-in-app/src/client/index.ts +++ b/packages/client/ui-open-in-app/src/client/index.ts @@ -25,7 +25,7 @@ import { OpenInAppPathController } from './open-path.ts' import { OpenPathAction, type OpenPathInjected } from './OpenPathAction.tsx' import { FileRouteAction } from './FileRouteAction.tsx' import { OpenPathEmptyAction } from './OpenPathEmptyAction.tsx' -import { en, ja, NS, zh, type OpenInAppKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type OpenInAppKey } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { @@ -51,7 +51,7 @@ export function apply(ctx: ClientContext): void { const controller = new OpenInAppController() void controller.load() const paths = new OpenInAppPathController(ctx.remote) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'open-in-app: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'open-in-app: dictionaries') const t = ctx.locale.bind(NS) const target = () => { if (ctx.layout.panelInfo.getSnapshot().activePanelId !== null) return undefined diff --git a/packages/client/ui-open-in-app/src/client/locales.ts b/packages/client/ui-open-in-app/src/client/locales.ts index f43d4746bb..737b027d00 100644 --- a/packages/client/ui-open-in-app/src/client/locales.ts +++ b/packages/client/ui-open-in-app/src/client/locales.ts @@ -99,3 +99,103 @@ export const ja = { 'app.filemanager': 'ファイル', 'app.terminal': 'ターミナル', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'open.title': 'Abrir en {app}', + 'path.appDefault': '{app} (predeterminado)', + 'path.appsError': 'No se pudieron cargar aplicaciones', + 'shortcut.busy': 'Abrir espacio de trabajo', + 'shortcut.unavailable': 'El espacio de trabajo actual o la aplicación local no están disponibles', + 'open.tooltip': 'Abierto localmente', + 'path.open': 'Abrir', + 'path.more': 'Más formas de abrir', + 'path.reveal': 'Mostrar ubicación del archivo', + 'path.openError': 'No se pudo abrir. Intentar otra vez.', + 'path.revealError': 'No se pudo mostrar la ubicación del archivo. Intentar otra vez.', + ...PRODUCT_NAMES, + 'app.finder': 'Finder', + 'app.explorer': 'Explorador de archivos', + 'app.filemanager': 'Archivos', + 'app.terminal': 'terminales', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'open.title': 'Ouvrir dans {app}', + 'path.appDefault': '{app} (par défaut)', + 'path.appsError': 'Impossible de charger les applications', + 'shortcut.busy': "Ouverture de l'espace de travail", + 'shortcut.unavailable': 'Espace de travail actuel ou application locale indisponible', + 'open.tooltip': 'Ouvrir localement', + 'path.open': 'Ouvrir', + 'path.more': "Plus de façons d'ouvrir", + 'path.reveal': "Afficher l'emplacement du fichier", + 'path.openError': "Impossible d'ouvrir. Essayer à nouveau.", + 'path.revealError': "Impossible d'afficher l'emplacement du fichier. Essayer à nouveau.", + ...PRODUCT_NAMES, + 'app.finder': 'Finder', + 'app.explorer': 'Explorateur de fichiers', + 'app.filemanager': 'Fichiers', + 'app.terminal': 'Borne', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'open.title': 'In {app} öffnen', + 'path.appDefault': '{app} (Standard)', + 'path.appsError': 'Anwendungen konnten nicht geladen werden', + 'shortcut.busy': 'Arbeitsbereich öffnen', + 'shortcut.unavailable': 'Aktueller Arbeitsbereich oder lokale Anwendung nicht verfügbar', + 'open.tooltip': 'Vor Ort geöffnet', + 'path.open': 'Öffnen', + 'path.more': 'Mehr Möglichkeiten zum Öffnen', + 'path.reveal': 'Dateispeicherort anzeigen', + 'path.openError': 'Konnte nicht geöffnet werden. Versuchen Sie es erneut.', + 'path.revealError': 'Der Dateispeicherort konnte nicht angezeigt werden. Versuchen Sie es erneut.', + ...PRODUCT_NAMES, + 'app.finder': 'Finder', + 'app.explorer': 'Datei-Explorer', + 'app.filemanager': 'Dateien', + 'app.terminal': 'Terminal', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'open.title': 'Открыть в {app}', + 'path.appDefault': '{app} (по умолчанию)', + 'path.appsError': 'Не удалось загрузить приложения', + 'shortcut.busy': 'Открытие рабочей области', + 'shortcut.unavailable': 'Текущая рабочая область или локальное приложение недоступно.', + 'open.tooltip': 'Открыть локально', + 'path.open': 'Открыть', + 'path.more': 'Больше способов открыть', + 'path.reveal': 'Показать местоположение файла', + 'path.openError': 'Не удалось открыть. Попробуйте еще раз.', + 'path.revealError': 'Не удалось показать местоположение файла. Попробуйте еще раз.', + ...PRODUCT_NAMES, + 'app.finder': 'Finder', + 'app.explorer': 'Проводник файлов', + 'app.filemanager': 'Файлы', + 'app.terminal': 'Терминал', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'open.title': 'Abrir em {app}', + 'path.appDefault': '{app} (padrão)', + 'path.appsError': 'Não foi possível carregar aplicativos', + 'shortcut.busy': 'Abrindo espaço de trabalho', + 'shortcut.unavailable': 'Espaço de trabalho atual ou aplicativo local indisponível', + 'open.tooltip': 'Abrir localmente', + 'path.open': 'Abrir', + 'path.more': 'Mais maneiras de abrir', + 'path.reveal': 'Mostrar localização do arquivo', + 'path.openError': 'Não foi possível abrir. Tente novamente.', + 'path.revealError': 'Não foi possível mostrar o local do arquivo. Tente novamente.', + ...PRODUCT_NAMES, + 'app.finder': 'Finder', + 'app.explorer': 'Explorador de arquivos', + 'app.filemanager': 'Arquivos', + 'app.terminal': 'Terminais', +} satisfies Record diff --git a/packages/client/ui-permission-presets/locale/de.json b/packages/client/ui-permission-presets/locale/de.json new file mode 100644 index 0000000000..0f2614b5c2 --- /dev/null +++ b/packages/client/ui-permission-presets/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle für den Berechtigungsmodus", + "description": "Legt die Standardberechtigungen für neue Sitzungen fest und wechselt den Berechtigungsmodus für die aktuelle Sitzung." + } +} diff --git a/packages/client/ui-permission-presets/locale/es.json b/packages/client/ui-permission-presets/locale/es.json new file mode 100644 index 0000000000..aed991a5c6 --- /dev/null +++ b/packages/client/ui-permission-presets/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de modo de permiso", + "description": "Establece los permisos predeterminados para nuevas sesiones y cambia el modo de permisos para la sesión actual." + } +} diff --git a/packages/client/ui-permission-presets/locale/fr.json b/packages/client/ui-permission-presets/locale/fr.json new file mode 100644 index 0000000000..8119ff4849 --- /dev/null +++ b/packages/client/ui-permission-presets/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface du mode autorisation", + "description": "Définit les autorisations par défaut pour les nouvelles sessions et change le mode d'autorisations pour la session en cours." + } +} diff --git a/packages/client/ui-permission-presets/locale/pt.json b/packages/client/ui-permission-presets/locale/pt.json new file mode 100644 index 0000000000..fc65c0a5d0 --- /dev/null +++ b/packages/client/ui-permission-presets/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface do modo de permissão", + "description": "Define as permissões padrão para novas sessões e alterna o modo de permissões para a sessão atual." + } +} diff --git a/packages/client/ui-permission-presets/locale/ru.json b/packages/client/ui-permission-presets/locale/ru.json new file mode 100644 index 0000000000..74ad24f1d4 --- /dev/null +++ b/packages/client/ui-permission-presets/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс режима разрешений", + "description": "Устанавливает разрешения по умолчанию для новых сеансов и переключает режим разрешений для текущего сеанса." + } +} diff --git a/packages/client/ui-permission-presets/src/client/index.ts b/packages/client/ui-permission-presets/src/client/index.ts index 940b5baa3f..8f4d98c854 100644 --- a/packages/client/ui-permission-presets/src/client/index.ts +++ b/packages/client/ui-permission-presets/src/client/index.ts @@ -41,7 +41,7 @@ import type { PermissionSelectInjected } from './PermissionSelect.tsx' import { PermissionRow } from './PermissionRow.tsx' import type { PermissionRowInjected } from './PermissionRow.tsx' import { - accessEn, accessJa, accessZh, en, ja, PERMISSION_ACCESS_NS, zh, + accessEn, accessJa, accessEs, accessFr, accessDe, accessRu, accessPt, accessZh, en, ja, es, fr, de, ru, pt, PERMISSION_ACCESS_NS, zh, } from './locales.ts' import { AUTO_REVIEW_PRESET, displayPermissionPreset, FULL_ACCESS_PRESET, @@ -113,7 +113,16 @@ function optionsOf( export function apply(ctx: ClientContext): void { const command = ctx.get('commandUi') as CommandUiContract const sessions = ctx.sessions - const accessDictionaries = { zh: accessZh, en: accessEn, ja: accessJa } + const accessDictionaries = { + zh: accessZh, + en: accessEn, + ja: accessJa, + es: accessEs, + fr: accessFr, + de: accessDe, + ru: accessRu, + pt: accessPt, + } ctx.effect( () => ctx.locale.register(PERMISSION_ACCESS_NS, accessDictionaries), 'ui-permission: current-session dictionaries', @@ -142,7 +151,7 @@ export function apply(ctx: ClientContext): void { 'ui-permission: dismiss stale slash choices', ) - ctx.effect(() => ctx.locale.register('settings.permission', { zh, en, ja }), 'ui-permission: settings row dictionaries') + ctx.effect(() => ctx.locale.register('settings.permission', { zh, en, ja, es, fr, de, ru, pt }), 'ui-permission: settings row dictionaries') // The shared ConfigForm mirror updates after document commits and reconnects. const controller = new PermissionPresetSettingsController( diff --git a/packages/client/ui-permission-presets/src/client/locales.ts b/packages/client/ui-permission-presets/src/client/locales.ts index 792c1d5605..9fe5025f87 100644 --- a/packages/client/ui-permission-presets/src/client/locales.ts +++ b/packages/client/ui-permission-presets/src/client/locales.ts @@ -99,6 +99,86 @@ export const ja = { 'confirm.enable': 'フルアクセスを有効にする', } satisfies Record +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'title': 'Permiso', + 'description': 'Elija el modo de permiso predeterminado para nuevas sesiones', + 'loading': 'Cargando', + 'unavailable': 'No disponible', + 'preset.readOnly': 'Sólo lectura', + 'preset.workspaceWrite': 'Escritura en el espacio de trabajo', + 'preset.fullAccess': 'Acceso completo', + 'confirm.title': '¿Habilitar acceso completo?', + 'confirm.description': 'El acceso completo permite que las nuevas sesiones reduzcan los pasos de confirmación y realicen más acciones directamente, incluidas operaciones confidenciales, cambios de archivos o comandos externos. Úselo sólo cuando confíe en las tareas posteriores.', + 'confirm.acknowledge': 'Entiendo los riesgos y quiero continuar.', + 'confirm.cancel': 'Cancelar', + 'confirm.enable': 'Habilitar acceso completo', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'title': 'Autorisation', + 'description': "Choisissez le mode d'autorisation par défaut pour les nouvelles sessions", + 'loading': 'Chargement', + 'unavailable': 'Indisponible', + 'preset.readOnly': 'Lecture seule', + 'preset.workspaceWrite': "Écriture dans l'espace de travail", + 'preset.fullAccess': 'Accès complet', + 'confirm.title': "Activer l'accès complet ?", + 'confirm.description': "L'accès complet permet aux nouvelles sessions de réduire les étapes de confirmation et d'effectuer directement davantage d'actions, notamment des opérations sensibles, des modifications de fichiers ou des commandes externes. Utilisez-le uniquement lorsque vous faites confiance aux tâches suivantes.", + 'confirm.acknowledge': 'Je comprends les risques et je veux continuer', + 'confirm.cancel': 'Annuler', + 'confirm.enable': "Activer l'accès complet", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'title': 'Erlaubnis', + 'description': 'Wählen Sie den Standardberechtigungsmodus für neue Sitzungen', + 'loading': 'Laden', + 'unavailable': 'Nicht verfügbar', + 'preset.readOnly': 'Nur lesen', + 'preset.workspaceWrite': 'Arbeitsbereich schreiben', + 'preset.fullAccess': 'Voller Zugriff', + 'confirm.title': 'Vollzugriff aktivieren?', + 'confirm.description': 'Durch den vollständigen Zugriff können neue Sitzungen die Bestätigungsschritte reduzieren und mehr Aktionen direkt ausführen, einschließlich vertraulicher Vorgänge, Dateiänderungen oder externer Befehle. Verwenden Sie es nur, wenn Sie nachfolgenden Aufgaben vertrauen.', + 'confirm.acknowledge': 'Ich verstehe die Risiken und möchte weitermachen', + 'confirm.cancel': 'Abbrechen', + 'confirm.enable': 'Aktivieren Sie Vollzugriff', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'title': 'Разрешение', + 'description': 'Выберите режим разрешений по умолчанию для новых сеансов', + 'loading': 'Загрузка', + 'unavailable': 'Недоступно', + 'preset.readOnly': 'Только чтение', + 'preset.workspaceWrite': 'Запись в рабочей области', + 'preset.fullAccess': 'Полный доступ', + 'confirm.title': 'Включить полный доступ?', + 'confirm.description': 'Полный доступ позволяет новым сеансам сократить количество шагов подтверждения и напрямую выполнять больше действий, включая конфиденциальные операции, изменение файлов или внешние команды. Используйте его только в том случае, если вы доверяете последующим задачам.', + 'confirm.acknowledge': 'Я понимаю риски и хочу продолжить', + 'confirm.cancel': 'Отмена', + 'confirm.enable': 'Включить полный доступ', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'title': 'Permissão', + 'description': 'Escolha o modo de permissão padrão para novas sessões', + 'loading': 'Carregando', + 'unavailable': 'Indisponível', + 'preset.readOnly': 'Somente leitura', + 'preset.workspaceWrite': 'Gravação no espaço de trabalho', + 'preset.fullAccess': 'Acesso total', + 'confirm.title': 'Ativar acesso total?', + 'confirm.description': 'O acesso total permite que novas sessões reduzam as etapas de confirmação e executem mais ações diretamente, incluindo operações confidenciais, alterações de arquivos ou comandos externos. Use-o apenas quando confiar nas tarefas subsequentes.', + 'confirm.acknowledge': 'Entendo os riscos e quero continuar', + 'confirm.cancel': 'Cancelar', + 'confirm.enable': 'Ativar acesso total', +} satisfies Record + /** Japanese copy, checked against the English keys. */ export const accessJa = { 'mode': 'アクセスモード、現在: {name}', @@ -119,3 +199,108 @@ export const accessJa = { 'auto.confirm.acknowledge': 'これらのリスクを理解し、続行します', 'auto.confirm.enable': '自動認可レビューを有効にする', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const accessEs = { + 'mode': 'Modo de acceso, actual: {name}', + 'close': 'Cerrar', + 'preset.readOnly': 'Sólo lectura', + 'preset.workspaceWrite': 'Escritura en el espacio de trabajo', + 'preset.fullAccess': 'Acceso completo', + 'confirm.title': '¿Habilitar acceso completo?', + 'confirm.description': 'El acceso completo reduce los pasos de confirmación y permite al agente realizar más acciones directamente, incluidas operaciones confidenciales, cambios de archivos o comandos externos. Úselo solo cuando confíe en la tarea actual.', + 'confirm.acknowledge': 'Entiendo los riesgos y quiero continuar.', + 'confirm.cancel': 'Cancelar', + 'confirm.enable': 'Habilitar acceso completo', + 'auto.label': 'Auto review', + 'auto.badge': 'EXP', + 'auto.description': 'Ejecute sin una zona de pruebas después de una revisión experimental del mismo modelo de cada llamada de herramienta nativa y llamada interna PTC.', + 'auto.confirm.title': '¿Habilitar Auto review (experimental)?', + 'auto.confirm.description': 'Auto review se ejecuta sin zona de pruebas. Antes de cada llamada a la herramienta nativa y llamada interna PTC, el mismo modelo que el agente actual revisa si se permite; usted aprueba o rechaza cada llamada que rechaza. Esta característica es experimental, puede permitir o denegar acciones falsamente y utiliza tokens adicionales.', + 'auto.confirm.acknowledge': 'Entiendo estos riesgos y quiero continuar', + 'auto.confirm.enable': 'Habilitar Auto review', +} satisfies typeof accessEn + +/** French dictionary, checked against the English keys. */ +export const accessFr = { + 'mode': "Mode d'accès, actuel : {name}", + 'close': 'Fermer', + 'preset.readOnly': 'Lecture seule', + 'preset.workspaceWrite': "Écriture dans l'espace de travail", + 'preset.fullAccess': 'Accès complet', + 'confirm.title': "Activer l'accès complet ?", + 'confirm.description': "L'accès complet réduit les étapes de confirmation et permet à l'agent d'effectuer directement davantage d'actions, notamment des opérations sensibles, des modifications de fichiers ou des commandes externes. Utilisez-le uniquement lorsque vous faites confiance à la tâche en cours.", + 'confirm.acknowledge': 'Je comprends les risques et je veux continuer', + 'confirm.cancel': 'Annuler', + 'confirm.enable': "Activer l'accès complet", + 'auto.label': 'Auto review', + 'auto.badge': 'EXP', + 'auto.description': "Exécutez sans bac à sable après un examen expérimental sur le même modèle de chaque appel d'outil natif et appel interne PTC.", + 'auto.confirm.title': 'Activer Auto review (expérimental) ?', + 'auto.confirm.description': "Auto review fonctionne sans bac à sable. Avant chaque appel d'outil natif et appel interne PTC, le même modèle que l'agent actuel vérifie s'il doit l'autoriser ; vous approuvez ou rejetez chaque appel qu'il refuse. Cette fonctionnalité est expérimentale, peut autoriser ou refuser à tort des actions et utilise des jetons supplémentaires.", + 'auto.confirm.acknowledge': 'Je comprends ces risques et je veux continuer', + 'auto.confirm.enable': 'Activer Auto review', +} satisfies typeof accessEn + +/** German dictionary, checked against the English keys. */ +export const accessDe = { + 'mode': 'Zugriffsmodus, aktuell: {name}', + 'close': 'Schließen', + 'preset.readOnly': 'Nur lesen', + 'preset.workspaceWrite': 'Arbeitsbereich schreiben', + 'preset.fullAccess': 'Voller Zugriff', + 'confirm.title': 'Vollzugriff aktivieren?', + 'confirm.description': 'Durch den Vollzugriff werden Bestätigungsschritte reduziert und der Agent kann mehr Aktionen direkt ausführen, einschließlich vertraulicher Vorgänge, Dateiänderungen oder externer Befehle. Verwenden Sie es nur, wenn Sie der aktuellen Aufgabe vertrauen.', + 'confirm.acknowledge': 'Ich verstehe die Risiken und möchte weitermachen', + 'confirm.cancel': 'Abbrechen', + 'confirm.enable': 'Aktivieren Sie Vollzugriff', + 'auto.label': 'Auto review', + 'auto.badge': 'EXP', + 'auto.description': 'Ausführung ohne Sandbox nach einer experimentellen, modellgleichen Überprüfung jedes nativen Toolaufrufs und PTC-Innenaufrufs.', + 'auto.confirm.title': 'Auto review aktivieren (experimentell)?', + 'auto.confirm.description': 'Auto review läuft ohne Sandbox. Vor jedem nativen Tool-Aufruf und PTC-Innenaufruf überprüft dasselbe Modell wie der aktuelle Agent, ob dies zulässig ist. Sie genehmigen oder lehnen jeden abgelehnten Aufruf ab. Diese Funktion ist experimentell, kann Aktionen fälschlicherweise zulassen oder verweigern und verwendet zusätzliche Token.', + 'auto.confirm.acknowledge': 'Ich verstehe diese Risiken und möchte weitermachen', + 'auto.confirm.enable': 'Aktivieren Sie Auto review', +} satisfies typeof accessEn + +/** Russian dictionary, checked against the English keys. */ +export const accessRu = { + 'mode': 'Режим доступа, текущий: {name}', + 'close': 'Закрыть', + 'preset.readOnly': 'Только чтение', + 'preset.workspaceWrite': 'Запись в рабочей области', + 'preset.fullAccess': 'Полный доступ', + 'confirm.title': 'Включить полный доступ?', + 'confirm.description': 'Полный доступ сокращает количество шагов подтверждения и позволяет агенту напрямую выполнять больше действий, включая конфиденциальные операции, изменения файлов или внешние команды. Используйте его только в том случае, если вы доверяете текущей задаче.', + 'confirm.acknowledge': 'Я понимаю риски и хочу продолжить', + 'confirm.cancel': 'Отмена', + 'confirm.enable': 'Включить полный доступ', + 'auto.label': 'Auto review', + 'auto.badge': 'EXP', + 'auto.description': 'Запуск без песочницы после экспериментальной проверки одной и той же модели каждого вызова встроенного инструмента и внутреннего вызова PTC.', + 'auto.confirm.title': 'Включить Auto review (экспериментально)?', + 'auto.confirm.description': 'Auto review работает без песочницы. Перед каждым вызовом собственного инструмента и внутренним вызовом PTC та же модель, что и текущий агент, проверяет, разрешить ли это; вы одобряете или отклоняете каждый вызов, который он отклоняет. Эта функция является экспериментальной, может ошибочно разрешать или запрещать действия и использует дополнительные токены.', + 'auto.confirm.acknowledge': 'Я понимаю эти риски и хочу продолжить', + 'auto.confirm.enable': 'Включить Auto review', +} satisfies typeof accessEn + +/** Portuguese dictionary, checked against the English keys. */ +export const accessPt = { + 'mode': 'Modo de acesso, atual: {name}', + 'close': 'Fechar', + 'preset.readOnly': 'Somente leitura', + 'preset.workspaceWrite': 'Gravação no espaço de trabalho', + 'preset.fullAccess': 'Acesso total', + 'confirm.title': 'Ativar acesso total?', + 'confirm.description': 'O acesso total reduz as etapas de confirmação e permite que o agente execute mais ações diretamente, incluindo operações confidenciais, alterações de arquivos ou comandos externos. Use-o apenas quando confiar na tarefa atual.', + 'confirm.acknowledge': 'Entendo os riscos e quero continuar', + 'confirm.cancel': 'Cancelar', + 'confirm.enable': 'Ativar acesso total', + 'auto.label': 'Auto review', + 'auto.badge': 'EXP', + 'auto.description': 'Execute sem um sandbox após uma revisão experimental do mesmo modelo de cada chamada de ferramenta nativa e PTC chamada interna.', + 'auto.confirm.title': 'Ativar Auto review (experimental)?', + 'auto.confirm.description': 'Auto review é executado sem sandbox. Antes de cada chamada de ferramenta nativa e chamada interna PTC, o mesmo modelo do agente atual analisa se deve permitir isso; você aprova ou rejeita cada chamada negada. Este recurso é experimental, pode permitir ou negar ações falsamente e usa tokens adicionais.', + 'auto.confirm.acknowledge': 'Entendo esses riscos e quero continuar', + 'auto.confirm.enable': 'Ativar Auto review', +} satisfies typeof accessEn diff --git a/packages/client/ui-plan/locale/de.json b/packages/client/ui-plan/locale/de.json new file mode 100644 index 0000000000..ce90df31f6 --- /dev/null +++ b/packages/client/ui-plan/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zur Planüberprüfung", + "description": "Zeigt Steuerelemente für den Planungsmodus, Planungskarten und eine Markdown-Vorschau an." + } +} diff --git a/packages/client/ui-plan/locale/es.json b/packages/client/ui-plan/locale/es.json new file mode 100644 index 0000000000..1bc8dde649 --- /dev/null +++ b/packages/client/ui-plan/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de revisión del plan", + "description": "Muestra controles del modo de planificación, tarjetas de planificación y vista previa de Markdown." + } +} diff --git a/packages/client/ui-plan/locale/fr.json b/packages/client/ui-plan/locale/fr.json new file mode 100644 index 0000000000..165c62a645 --- /dev/null +++ b/packages/client/ui-plan/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de révision des plans", + "description": "Affiche les commandes du mode planification, les cartes de planification et l'aperçu Markdown." + } +} diff --git a/packages/client/ui-plan/locale/pt.json b/packages/client/ui-plan/locale/pt.json new file mode 100644 index 0000000000..1ad0ee2f88 --- /dev/null +++ b/packages/client/ui-plan/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de revisão do plano", + "description": "Mostra controles do modo de planejamento, cartões de planejamento e visualização do Markdown." + } +} diff --git a/packages/client/ui-plan/locale/ru.json b/packages/client/ui-plan/locale/ru.json new file mode 100644 index 0000000000..08eba25394 --- /dev/null +++ b/packages/client/ui-plan/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс проверки плана", + "description": "Показывает элементы управления режимом планирования, карты планирования и предварительный просмотр Markdown." + } +} diff --git a/packages/client/ui-plan/src/client/index.ts b/packages/client/ui-plan/src/client/index.ts index 1b226f8a37..34196a00da 100644 --- a/packages/client/ui-plan/src/client/index.ts +++ b/packages/client/ui-plan/src/client/index.ts @@ -25,7 +25,7 @@ import { planAddress, parsePlanAddress } from './plan.ts' import { isReviewPreviewAddress, reviewPreviewAddress } from './review-preview.ts' import { createPlanReviewStore } from './review-store.ts' import { PlanChip } from './PlanModeControl.tsx' -import { en, ja, zh, type PlanKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type PlanKey } from './locales.ts' export type { PlanKey } from './locales.ts' @@ -56,7 +56,7 @@ export const inject = ['slots', 'remote', 'remote.commands', 'remote.session', ' * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-plan: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-plan: dictionaries') const previewId = '@deepseek-ai/dsh-client-ui-plan' const t = ctx.locale.bind(NS) diff --git a/packages/client/ui-plan/src/client/locales.ts b/packages/client/ui-plan/src/client/locales.ts index 331fdb5234..5940519fa2 100644 --- a/packages/client/ui-plan/src/client/locales.ts +++ b/packages/client/ui-plan/src/client/locales.ts @@ -65,3 +65,108 @@ export const ja = { 'chip.on.title': '計画モードはオンです — クリックでオフ (/plan off)', 'chip.exitFailed': '計画モードを終了できませんでした', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'chip.label': 'Planificar', + 'preview.title': 'Planificar', + 'preview.document': 'Plano · Markdown', + 'preview.action': 'Abrir', + 'preview.open': 'Plan abierto en la barra lateral', + 'preview.full': 'Ver plano completo', + 'preview.openNamed': 'Plan abierto: {title}', + 'preview.loading': 'Cargando plan…', + 'preview.failed': 'No se pudo cargar el plan', + 'preview.invalidAddress': 'Dirección del plan no válida', + 'preview.historyUnavailable': 'El historial de sesiones no está disponible', + 'preview.notFound': 'Este plan no fue encontrado.', + 'preview.unavailable': 'La vista previa del plan no está disponible', + 'preview.expired': 'Esta vista previa del plan temporal ha caducado. Vuelva a abrirlo desde la tarjeta de revisión pendiente.', + 'chip.on.aria': 'Modo plan activado, presione para apagar', + 'chip.on.title': 'Modo de planificación activado: haga clic para desactivar (/plan off)', + 'chip.exitFailed': 'No se pudo salir del modo de plan', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'chip.label': 'Planifier', + 'preview.title': 'Planifier', + 'preview.document': 'Plan · Markdown', + 'preview.action': 'Ouvrir', + 'preview.open': 'Plan ouvert dans la barre latérale', + 'preview.full': 'Voir le forfait complet', + 'preview.openNamed': 'Plan ouvert : {title}', + 'preview.loading': 'Plan de chargement…', + 'preview.failed': 'Impossible de charger le plan', + 'preview.invalidAddress': 'Adresse du forfait invalide', + 'preview.historyUnavailable': "L'historique des sessions n'est pas disponible", + 'preview.notFound': "Ce plan n'a pas été trouvé", + 'preview.unavailable': "L'aperçu du plan n'est pas disponible", + 'preview.expired': 'Cet aperçu du plan temporaire a expiré. Rouvrez-le à partir de la carte de révision en attente.', + 'chip.on.aria': 'Mode Plan activé, appuyez pour désactiver', + 'chip.on.title': 'Mode Plan activé — cliquez pour désactiver (/plan off)', + 'chip.exitFailed': 'Échec de la sortie du mode Plan', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'chip.label': 'Planen', + 'preview.title': 'Planen', + 'preview.document': 'Plan · Markdown', + 'preview.action': 'Öffnen', + 'preview.open': 'Plan in der Seitenleiste öffnen', + 'preview.full': 'Vollständigen Plan anzeigen', + 'preview.openNamed': 'Offener Grundriss: {title}', + 'preview.loading': 'Ladeplan…', + 'preview.failed': 'Plan konnte nicht geladen werden', + 'preview.invalidAddress': 'Ungültige Planadresse', + 'preview.historyUnavailable': 'Der Sitzungsverlauf ist nicht verfügbar', + 'preview.notFound': 'Dieser Plan wurde nicht gefunden', + 'preview.unavailable': 'Die Planvorschau ist nicht verfügbar', + 'preview.expired': 'Diese temporäre Planvorschau ist abgelaufen. Öffnen Sie es erneut über die ausstehende Überprüfungskarte.', + 'chip.on.aria': 'Planmodus ein, zum Ausschalten drücken', + 'chip.on.title': 'Planmodus ein – zum Ausschalten klicken (/plan off)', + 'chip.exitFailed': 'Der Planmodus konnte nicht beendet werden', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'chip.label': 'План', + 'preview.title': 'План', + 'preview.document': 'План · Markdown', + 'preview.action': 'Открыть', + 'preview.open': 'Открытая планировка в боковой панели', + 'preview.full': 'Посмотреть полный план', + 'preview.openNamed': 'Открытая планировка: {title}', + 'preview.loading': 'План загрузки…', + 'preview.failed': 'Не удалось загрузить план', + 'preview.invalidAddress': 'Неверный адрес плана', + 'preview.historyUnavailable': 'История сеансов недоступна', + 'preview.notFound': 'Этот план не найден', + 'preview.unavailable': 'Предварительный просмотр плана недоступен.', + 'preview.expired': 'Срок действия этой временной предварительной версии плана истек. Откройте его снова из карточки ожидающего рассмотрения.', + 'chip.on.aria': 'Режим планирования включен. Нажмите, чтобы выключить.', + 'chip.on.title': 'Режим планирования включен — нажмите, чтобы выключить (/plan off)', + 'chip.exitFailed': 'Не удалось выйти из режима планирования.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'chip.label': 'Plano', + 'preview.title': 'Plano', + 'preview.document': 'Plano · Markdown', + 'preview.action': 'Abrir', + 'preview.open': 'Abrir plano na barra lateral', + 'preview.full': 'Ver plano completo', + 'preview.openNamed': 'Plano aberto: {title}', + 'preview.loading': 'Carregando plano…', + 'preview.failed': 'Não foi possível carregar o plano', + 'preview.invalidAddress': 'Endereço do plano inválido', + 'preview.historyUnavailable': 'O histórico da sessão não está disponível', + 'preview.notFound': 'Este plano não foi encontrado', + 'preview.unavailable': 'A visualização do plano não está disponível', + 'preview.expired': 'Esta visualização temporária do plano expirou. Reabra-o no cartão de revisão pendente.', + 'chip.on.aria': 'Modo de planejamento ativado, pressione para desligar', + 'chip.on.title': 'Modo de planejamento ativado – clique para desligar (/plan off)', + 'chip.exitFailed': 'Falha ao sair do modo de plano', +} satisfies Record diff --git a/packages/client/ui-plugin-manager/locale/de.json b/packages/client/ui-plugin-manager/locale/de.json new file mode 100644 index 0000000000..16a03e8049 --- /dev/null +++ b/packages/client/ui-plugin-manager/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Plug-in-Verwaltungsschnittstelle", + "description": "Plug-in-Pakete anzeigen und installieren sowie Vorgänge aktivieren, deaktivieren und wiederholen." + } +} diff --git a/packages/client/ui-plugin-manager/locale/es.json b/packages/client/ui-plugin-manager/locale/es.json new file mode 100644 index 0000000000..3ca20e033d --- /dev/null +++ b/packages/client/ui-plugin-manager/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de gestión de complementos", + "description": "Vea e instale paquetes de complementos y habilite, deshabilite y reintente operaciones." + } +} diff --git a/packages/client/ui-plugin-manager/locale/fr.json b/packages/client/ui-plugin-manager/locale/fr.json new file mode 100644 index 0000000000..48bf4551cd --- /dev/null +++ b/packages/client/ui-plugin-manager/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de gestion des plug-ins", + "description": "Affichez et installez les packages de plug-ins, puis activez, désactivez et réessayez les opérations." + } +} diff --git a/packages/client/ui-plugin-manager/locale/pt.json b/packages/client/ui-plugin-manager/locale/pt.json new file mode 100644 index 0000000000..f1e246b3a2 --- /dev/null +++ b/packages/client/ui-plugin-manager/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de gerenciamento de plug-ins", + "description": "Visualize e instale pacotes de plug-ins e ative, desative e repita as operações." + } +} diff --git a/packages/client/ui-plugin-manager/locale/ru.json b/packages/client/ui-plugin-manager/locale/ru.json new file mode 100644 index 0000000000..155d905c69 --- /dev/null +++ b/packages/client/ui-plugin-manager/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс управления плагинами", + "description": "Просмотрите и установите пакеты подключаемых модулей, а также включите, отключите и повторите операции." + } +} diff --git a/packages/client/ui-plugin-manager/src/client/index.ts b/packages/client/ui-plugin-manager/src/client/index.ts index 54dce2da8b..f5908227a4 100644 --- a/packages/client/ui-plugin-manager/src/client/index.ts +++ b/packages/client/ui-plugin-manager/src/client/index.ts @@ -30,7 +30,7 @@ import { PluginsPanelIcon } from './PluginsPanelIcon.tsx' import { configLedgerSource } from './config-ledger.ts' import { tabLedgerSource } from './tab-ledger.ts' import { PluginManagerController } from './manager-store.ts' -import { en, ja, zh, type PluginManagerLocaleKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type PluginManagerLocaleKey } from './locales.ts' import { createNavigationStore } from './navigation-store.ts' import type {} from './slot-contract.ts' @@ -92,7 +92,7 @@ export const inject = ['slots', 'locale', 'remote', 'remote.pluginManager', 'rem * @param ctx - the browser plugin context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-plugin-manager: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-plugin-manager: dictionaries') const t = ctx.locale.bind(NS) const controller = new PluginManagerController(ctx) ctx.effect(() => () => { controller.dispose() }, 'ui-plugin-manager: controller') diff --git a/packages/client/ui-plugin-manager/src/client/locales.ts b/packages/client/ui-plugin-manager/src/client/locales.ts index 912baa2c35..a871ab3692 100644 --- a/packages/client/ui-plugin-manager/src/client/locales.ts +++ b/packages/client/ui-plugin-manager/src/client/locales.ts @@ -880,3 +880,1458 @@ export const ja = { 'detailFailed': '詳細を読み込めませんでした。', 'detailFootnotes': '脚注', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + panel: 'Complementos', + title: 'Complementos', + intro: 'Instalar, habilitar y configurar complementos', + infoLabel: 'Acerca de los complementos', + infoDescription: 'Configure complementos oficiales e instale o administre otros complementos aquí. Vea la lista de complementos integrados y el estado del tiempo de ejecución en Configuración → Complementos integrados.', + loading: 'Leyendo complementos…', + error: 'No se pudieron leer todos los complementos, posiblemente debido a un problema de red', + unavailable: 'Esta implementación se ejecuta sin un perfil manejable, por lo que los complementos no se pueden instalar ni cambiar aquí.', + retry: 'Reintentar', + refresh: 'Actualizar', + refreshError: 'Error al actualizar. Por favor inténtalo de nuevo.', + empty: 'Aún no hay complementos instalados.', + addPlugin: 'Agregar complemento', + chooseAddMethod: 'Elija cómo agregar un complemento', + installExisting: 'Instalar un complemento de terceros', + installExistingDescription: 'Admite paquetes npm, repositorios Git y directorios locales', + restartNotice: 'Reinicie DSH para aplicar el cambio. {reason}', + restartReasonNoHmr: 'Este perfil aplica cambios solo al inicio.', + restartReasonPackageMoved: 'Un paquete instalado ahora se resuelve en otro directorio o el cambio reemplaza un paquete que proporciona la instalación de DSH. Desinstale un paquete antes de instalarlo desde otra ubicación.', + restartReasonHostModule: 'El cambio llega a los módulos DSH propiamente cargados.', + restartReasonUnmanagedPlugin: 'El cambio afecta a un complemento que se ejecuta fuera de la lista de complementos.', + restartReasonModuleComparison: 'DSH no pudo comparar los complementos cargados con sus archivos.', + overriddenNotice: '{name} se guardó, pero una configuración de mayor prioridad lo anula, por lo que no está vigente', + bundlesTitle: 'Complementos instalados', + basicTitle: 'Configuración del complemento incorporado', + extensionsTitle: 'Complementos experimentales', + moreTitle: 'Complementos experimentales', + moreAction: 'Más', + tabsLabel: 'Vistas de complementos', + installedTab: 'Instalado', + statusProblem: 'problema', + statusBeta: 'Experimental', + reasonLabel: 'Razón', + metadataError: 'Error de metadatos del paquete: {error}', + versionTag: 'v{version}', + partsLabel: 'Componentes', + sourceTitle: 'Fuente', + sourceSpec: 'fuente del código', + sourceBuiltIn: 'Construido en', + sourceVersion: 'Versión actual', + partsEmpty: 'Este paquete de complementos no contiene componentes.', + partsCountTotal: '{count} total', + partsCountRunning: '{count} corriendo', + partsCountOff: '{count} apagado', + partOff: 'Apagado', + partsCountFailed: '{count} falló', + partsFilter: 'Componentes del filtro', + partsFilterEmpty: 'Ningún componente coincide.', + partToggle: 'Habilitar componente {name}', + rowPhasePending: 'Esperando dependencias', + rowPhaseLoading: 'Cargando', + rowPhaseActive: 'corriendo', + rowPhaseFailed: 'problema', + rowPhaseUnloading: 'Descarga', + enableToggle: 'Habilitar {name}', + applyingChange: 'Aplicar cambios a {name}', + openDetail: 'Ver {name}', + backToList: 'Volver a complementos', + crumbRoot: 'Complementos', + backToPackage: 'Volver a {name}', + configureRow: 'Configurar {name}', + rowStateIdle: 'no corriendo', + uninstall: 'Desinstalar', + uninstallLabel: 'Desinstalar {name}', + installTitle: 'Agregar complemento', + installDescription: 'Ingrese el nombre del paquete del complemento, la dirección del repositorio GitHub o la ruta del directorio local.', + installSpecLabel: 'Nombre o dirección del paquete', + installSpecPlaceholder: 'por ejemplo dsh-plugin-ballena-mascota', + installGuideToggle: 'Guía de instalación y ejemplos.', + installGuideHide: 'Ocultar la guía', + installGuideIdTitle: 'Ingrese el nombre del paquete npm del complemento', + installGuideIdExample: 'dsh-plugin-ballena-mascota', + installGuideIdHint: 'El nombre del paquete del complemento es el nombre del paquete npm (como dsh-xxx o @author/plugin): la parte después de dsh plugin add o pnpm add en el comando de instalación README de un complemento comunitario.', + installGuideExampleLabel: 'Ejemplo: ', + installGitTemplateHint: 'Reemplace esto con la dirección real del repositorio de Git.', + installPathTemplateHint: 'Reemplace esto con la ruta real a su directorio de complementos local.', + installGuideFill: 'ejemplo de uso', + installGuideFillAria: 'Utilice el ejemplo {example}', + installGuideSafety: 'Instale sólo complementos en los que confíe: se ejecutan con sus permisos y pueden dañar DeepSeek Harness o filtrar sus datos.', + installUpgradeNotice: 'Los complementos oficiales bajo demanda ofrecen actualización cuando su versión difiere de DSH. Para actualizar otros complementos, desinstálelos y vuelva a instalarlos.', + registryToggle: 'Registro', + registryLegend: 'El registro npm desde el que se descarga el complemento', + registryDefault: 'Registro predeterminado', + registryOfficial: 'Registro oficial de npm', + registryNpmmirror: 'Espejo de China continental', + registryCustom: 'Dirección personalizada', + registryCustomPlaceholder: 'https://npm.example.com/', + registryCustomHint: 'Ingrese una dirección de registro npm interna o privada que comience con http:// o https://.. Si requiere iniciar sesión, almacene las credenciales en ~/.npmrc en esta máquina.', + registryCustomInvalid: 'Introduzca una dirección que comience con http:// o https://', + registryListSeparator: ', ', + sentenceSeparator: ' ', + installRun: 'Instalar', + installChecking: 'Comprobando…', + installConfirmedTitle: 'Instalar complemento', + installConfirmedRefused: 'Este complemento no se puede instalar', + installProblemInvalid: 'Este no es un nombre o dirección de paquete que se pueda instalar: {reason}', + installProblemInstalled: 'Este complemento ya está instalado. Para actualizarlo, desinstálelo e instálelo nuevamente.', + installProblemShipped: 'Este complemento se envía con DSH; actualizar DSH lo actualiza', + installBundledTitle: 'Este complemento se envía con DSH', + installBundledDescription: 'Habilítelo sin descargar otra copia', + installProblemNotFound: 'No se encontró tal complemento', + installProblemNotPackage: 'La ruta no existe o no es un paquete de complemento válido', + installProblemNotBundle: 'Este paquete no declara ningún paquete, por lo que no se puede instalar como complemento: {reason}', + installProblemNetwork: 'No se pudo acceder al registro del complemento; comprueba la red y vuelve a intentarlo', + installProblemNetworkAll: 'No se pudo acceder a ningún registro (probado: {registries}); verifique la configuración de red o proxy, o cambie el registro', + installProblemUnknown: 'No se pudo buscar el complemento: {reason}', + installingTitle: 'Instalando el complemento…', + installedTitle: 'Instalado', + statusNotInstalled: 'No instalado', + statusMissingFiles: 'Paquete no disponible', + statusUpdateAvailable: 'Actualización disponible', + sourceNotInstalled: 'No instalado', + sourceTargetSpec: 'Fuente de este DSH', + sourceTargetVersion: 'Versión para este DSH', + update: 'Actualizar', + updateLabel: 'Actualización {name}', + versionsTitle: 'Versiones', + versionsRecheck: 'comprobar de nuevo', + versionsChecking: 'Buscando actualizaciones', + versionsAvailable: 'Actualización a {version} disponible', + versionsPublished: 'Publicado {date}', + versionsLatest: 'actualizado', + versionsCheckedAt: 'Última comprobación en {time}', + versionsBlocked: 'Las versiones más nuevas necesitan una actualización DSH', + versionsBlockedDetail: '{version} necesita {peers}; este DSH es {runtime}', + versionsNoneInstallable: 'No hay una versión más nueva para instalar directamente', + versionsNoneInstallableDetail: 'Las versiones más recientes están obsoletas, son versiones preliminares o no declaran ningún paquete. Ampliar para verlos', + versionsFailedNetwork: 'No se pudo acceder al registro', + versionsFailedOther: 'No se pudo buscar actualizaciones', + versionsDetails: 'Detalles', + versionsNewerCount: '{count} más nuevo', + versionsCollapse: 'Contraer', + versionsInstalledTag: 'Instalado', + versionsPrereleaseTag: 'Presentación', + versionsDeprecatedTag: 'En desuso', + versionsNeedsDsh: 'Necesita {peers}', + versionsNotBundle: 'Esta versión no declara ningún paquete y no se puede instalar.', + versionsUpdateLabel: 'Actualización a {version}', + versionsRegistry: 'Registro', + versionsCheckedAtLabel: 'Comprobado en', + versionsFallbackTag: 'Respaldo', + versionsFallbackHint: 'No se pudo alcanzar {registry}; la lista proviene de un registro alternativo', + versionsPrimaryTag: 'primero', + versionsShowPrerelease: 'Mostrar presentaciones preliminares', + versionsOmitted: '{count} no se enumeran más versiones más recientes que la instalada', + versionsProblemNetwork: 'Inalcanzable', + versionsProblemNotFound: 'No encontrado', + versionsProblemUnknown: 'Fallido', + updatesAvailable: 'Actualizaciones disponibles: {count}', + updatesNone: 'Todos los complementos están actualizados.', + updatesFailed: 'Algunos complementos no se pudieron verificar', + updateTitle: 'Actualización {name}', + updateSource: 'Instalar desde', + updateDescription: 'La actualización reemplaza {from} por {to}; el complemento puede detenerse brevemente. Si la instalación falla, {from} se conserva o se vuelve a instalar.', + updateRunning: 'Actualizando', + updateStepChecking: 'Comprobar {version}', + updateStepRemoving: 'Detener y eliminar {version}', + updateStepInstalling: 'Instale {version} desde {registry}', + updateStepRestoring: 'Instale {version} nuevamente', + updateDone: 'Actualizado a {version}', + updateDoneDetail: 'La nueva versión se está ejecutando.', + updateRestart: '{version} instalado; entra en vigor después de un reinicio', + updateRefused: 'No se puede actualizar a {version}', + updateRemoveFailed: 'No se pudo eliminar {version}', + updateRestored: 'La actualización falló; restaurado {version}', + updateRestoredDetail: '{to} no pudo instalarse; {from} se instaló nuevamente.', + updateKept: 'No se pudo actualizar a {version}', + updateKeptDetail: '{from} no se eliminó y permanece en uso. {reason}', + updateBroken: 'La actualización falló; el complemento no está instalado', + updateBrokenDetail: '{to} no se pudo instalar y la instalación de {from} nuevamente también falló. Instale {name} nuevamente desde Agregar complemento.', + updateLost: 'Se desconoce el resultado de la actualización.', + updateLostDetail: 'Se interrumpió la conexión con DSH: {reason}. Actualice la lista de complementos para ver la versión instalada.', + updateOutput: 'Mostrar resultado de instalación', + updateFinish: 'hecho', + installFailedTitle: 'No se pudo instalar el complemento', + installGithubFailedTitle: 'No se puede acceder a GitHub', + installGithubTimeoutTitle: 'Se agotó el tiempo de espera de la conexión GitHub', + installGithubFailedDescription: 'Pruebe con otra fuente de instalación.', + installUseGithubMirror: 'Utilice el espejo de China continental', + installTryAnotherWay: 'Prueba de otra manera', + installPackageLabel: 'Nombre del paquete del complemento', + installEdit: 'Editar', + installEditAria: 'Volver a editar', + installCancelAndEdit: 'Cancelar la instalación y volver a editar', + installApplyingCancellationError: 'La cancelación no fue confirmada. Se está aplicando la instalación; espera su resultado. {reason}', + installReconcile: 'Verificar el estado de la instalación', + installUnknownTitle: 'El resultado de la instalación no está disponible', + installUnknownDescription: 'El Host no tiene ninguna instalación activa con esta identificación de solicitud. Consulte la lista de complementos antes de volver a intentarlo.', + installResultUnconfirmed: 'No se recibió el resultado de la instalación. Verificar el estado de la instalación. {reason}', + installAwaitingAcceptance: 'Esperando que el Host acepte la instalación. La cancelación se volverá a intentar automáticamente después de la confirmación.', + installBackgroundUnknown: 'El resultado de la instalación no está disponible. Consulte la lista de complementos.', + installCancel: 'Cancelar instalación', + installCloseCancels: 'Cancelar instalación y cerrar', + installViewTask: 'Ver instalación', + installUnconfirmedTitle: 'Estado de instalación no confirmado', + installBackgroundDone: 'Instalación finalizada. Ver detalles de instalación.', + installBackgroundFailed: 'La instalación falló. Ver detalles de instalación.', + installBackgroundUnconfirmed: 'El estado de la instalación no está confirmado. Vea la instalación para más detalles.', + installBackgroundApplying: 'La instalación se está aplicando y no se puede cancelar. Ver el progreso de la instalación.', + installStarting: 'Preparando la instalación…', + installCancelling: 'Deteniendo la instalación…', + installApplying: 'Aplicando configuración, espere...', + installCancelledShort: 'Cancelado', + installCancelled: 'Instalación cancelada; el complemento no está habilitado y los archivos descargados pueden permanecer', + installCancelUnconfirmed: 'No se ha confirmado que la instalación se haya detenido. Vuelva a intentar la cancelación o espere el resultado de la instalación. {reason}', + installEnableNow: 'Habilitar ahora', + installDetailsShow: 'Mostrar detalles de instalación', + installDetailsHide: 'Ocultar detalles de instalación', + installVersion: 'Versión {version}', + installSubjectPath: 'directorio local', + installSubjectGit: 'repositorio git', + installSubjectTarball: 'bola de tar', + installLocation: 'Se instala en {dir}', + installRetry: 'Reintentar', + installChangeRegistry: 'Cambiar registro', + installAttempt: '{previous} no pudo entregar el paquete; reintentando a través de {registry} (registro {index} de {total})', + installAttemptBadge: 'Intento {index} · {registry}', + installFailureNetwork: 'La conexión de red falló', + installFailureNetworkAll: 'No se pudo acceder a ningún registro (probado: {registries}). Verifique la configuración de red o proxy, o cambie el registro y vuelva a intentarlo.', + installFailureNetworkHost: 'No se pudo contactar con {host}. Una dirección GitHub o un enlace .tgz no se recupera a través del registro: esta máquina debe llegar directamente o mediante un proxy. Si el complemento también está publicado en npm, ingrese el nombre del paquete.', + installFailureNotFound: 'No se encontró tal complemento', + installFailureNoMatchingVersion: 'Ninguna versión coincide con la solicitud', + installFailureDiskFull: 'El disco está lleno; la instalación se detuvo', + installFailurePermission: 'Sin permiso de escritura; el complemento no se puede instalar', + installFailureBuildBlocked: 'Los scripts de instalación de una dependencia necesitan su permiso antes de que la instalación pueda continuar', + installFailureBuildBlockedManual: 'pnpm bloqueó los scripts de instalación; permítalos en enableBuilds en pnpm-workspace.yaml y vuelva a intentarlo', + installFailureIntegrity: 'El paquete descargado no pasó la verificación de integridad', + installFailureTimeout: 'La instalación expiró', + installFailurePnpmMissing: 'No se encontró pnpm, por lo que no se puede instalar nada.', + installFailureGeneric: 'Algo salió mal durante la instalación; los detalles dicen lo que', + terminalCommandLine: 'Línea de comando {n}', + terminalRunning: 'corriendo', + terminalFailed: 'Fallido', + terminalDone: 'hecho', + terminalCopy: 'Copiar', + terminalCopied: 'Copiado', + terminalNoOutput: 'Sin salida', + terminalCollapseAria: 'Contraer salida', + terminalCollapse: 'Contraer', + terminalExpandAria: 'Expanda las líneas de salida {n} restantes', + terminalExpand: '… {n} más líneas', + terminalExitCode: 'código de salida {code}', + terminalSignal: 'señal {signal}', + terminalNoExitCode: 'sin código de salida', + installDoneNothing: 'Instalación finalizada sin nuevas dependencias.', + installDoneRestart: 'Instalado; reinicie DSH para cargarlo. {reason}', + installDoneApproved: 'Instalar scripts permitidos para {names}', + installDoneOtherVersion: 'Según la política de seguridad de la cadena de suministro de pnpm (minimumReleaseAge), las versiones publicadas más recientemente que la antigüedad requerida no se instalan automáticamente, por lo que esta instalación utilizó {installed} en lugar de la última versión {version}. Una versión recién publicada no ha completado este período de espera y puede contener códigos maliciosos o defectos no detectados. Si necesita {version}, desinstale la versión actual y luego instale {exact} para solicitar esa versión.', + installApprovalTitle: 'Instalar scripts necesita permiso', + installApprovalDescription: 'Estos paquetes tienen scripts de instalación que pnpm no ejecutó.', + installApprovalConsequence: 'Una vez permitidos, los scripts se ejecutan aquí con sus permisos y el permiso se guarda en este perfil.', + installApprovalCaution: 'Permita solo paquetes en los que confíe.', + installApproveAndRetry: 'Permita estos scripts y vuelva a intentarlo', + installClose: 'hecho', + close: 'Cerrar', + cancel: 'Cancelar', + confirmUninstallTitle: '¿Desinstalar "{name}"?', + confirmUninstallDescription: 'Lo que proporciona desaparece una vez que se desinstala.', + confirmUninstall: 'Desinstalar', + failedEnable: 'No se pudo habilitar: {reason}', + failedDisable: 'No se pudo desactivar: {reason}', + failedUninstall: 'No se pudo desinstalar: {reason}', + failedRowEnable: 'No se pudo habilitar el componente: {reason}', + failedRowDisable: 'No se pudo desactivar el componente: {reason}', + reasonManagementRequired: 'La gestión de complementos lo necesita; no se puede apagar ni desinstalar.', + reasonUnaddressable: 'El parche de perfil no puede abordar este de forma única.', + reasonUnknownPlugin: 'No existe tal complemento.', + reasonInvalidSpec: 'Ingrese un nombre o dirección de paquete válido.', + reasonAlreadyInstalled: 'Este paquete ya está instalado; desinstálelo antes de instalar otra versión.', + reasonAmbiguousInstall: 'No se puede saber qué paquete se instaló a partir del cambio de dependencia.', + reasonNotBundle: 'Este paquete no declara ningún paquete, por lo que no se puede administrar como un complemento.', + reasonNotRemovable: 'Este paquete no es propiedad del perfil o la administración del complemento lo necesita.', + reasonStopProfile: 'Este perfil se ejecuta sin HMR; deténgalo y desinstale el paquete con el complemento dsh.', + reasonBundleInUse: 'Otra configuración todavía usa los componentes de este paquete; apáguelos primero.', + reasonStaleApproval: 'Las aprobaciones de guiones pendientes cambiaron; instálelo nuevamente para actualizarlos.', + reasonIncompatibleVersion: '{plugin} es incompatible con DSH {runtime} (requiere {requirements}); ejecutarlo puede causar fallas o pérdida de datos.', + reasonIncompatibleVersionUnnamed: 'Este complemento es incompatible con la versión DSH en ejecución; ejecutarlo puede causar fallas o pérdida de datos.', + reasonIncompatibleInstall: 'Instale una versión del complemento compatible con este DSH.', + reasonIncompatibleInstalled: 'Desinstálalo e instala una versión compatible con este DSH.', + reasonOperationError: 'El anfitrión informó un error.', + detailTitle: 'Detalles', + detailLoading: 'Detalles de lectura', + detailTooLarge: 'Los detalles exceden el límite de tamaño y no se cargaron.', + detailNetwork: 'No se pudo llegar a la fuente de los detalles.', + detailInvalid: 'El complemento declara un archivo de detalles no válido.', + detailFailed: 'No se pudieron leer los detalles.', + detailFootnotes: 'Notas a pie de página', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + panel: 'Plugins', + title: 'Plugins', + intro: 'Installer, activer et configurer les plugins', + infoLabel: 'À propos des plugins', + infoDescription: "Configurez les plugins officiels et installez ou gérez d'autres plugins ici. Affichez la liste des plugins intégrés et l'état d'exécution dans Paramètres → Plugins intégrés.", + loading: 'Plugins de lecture…', + error: "Impossible de lire tous les plugins, probablement à cause d'un problème de réseau", + unavailable: "Ce déploiement s'exécute sans profil gérable, les plugins ne peuvent donc pas être installés ou commutés ici.", + retry: 'Réessayer', + refresh: 'Actualiser', + refreshError: "L'actualisation a échoué. Veuillez réessayer.", + empty: "Aucun plugin n'est encore installé.", + addPlugin: 'Ajouter un plugin', + chooseAddMethod: 'Choisissez comment ajouter un plugin', + installExisting: 'Installer un plugin tiers', + installExistingDescription: 'Prend en charge les packages npm, les référentiels Git et les répertoires locaux', + restartNotice: 'Redémarrez DSH pour appliquer la modification. {reason}', + restartReasonNoHmr: 'Ce profil applique les modifications uniquement au démarrage.', + restartReasonPackageMoved: "Un package installé est désormais résolu dans un autre répertoire ou la modification remplace un package fourni par l'installation de DSH. Désinstallez un package avant de l’installer à partir d’un autre emplacement.", + restartReasonHostModule: 'Le changement atteint les modules DSH lui-même chargés.', + restartReasonUnmanagedPlugin: 'La modification atteint un plugin exécuté en dehors de la liste des plugins.', + restartReasonModuleComparison: "DSH n'a pas pu comparer les plugins chargés avec leurs fichiers.", + overriddenNotice: "{name} a été enregistré, mais une configuration de priorité plus élevée l'écrase, elle n'est donc pas effective", + bundlesTitle: 'Plugins installés', + basicTitle: 'Paramètres du plugin intégré', + extensionsTitle: 'Plugins expérimentaux', + moreTitle: 'Plugins expérimentaux', + moreAction: 'Plus', + tabsLabel: 'Vues des plugins', + installedTab: 'Installé', + statusProblem: 'Problème', + statusBeta: 'Expérimental', + reasonLabel: 'Raison', + metadataError: 'Erreur de métadonnées du package : {error}', + versionTag: 'v{version}', + partsLabel: 'Composants', + sourceTitle: 'Source', + sourceSpec: 'Source du code', + sourceBuiltIn: 'Intégré', + sourceVersion: 'Version actuelle', + partsEmpty: 'Ce pack de plugins ne contient aucun composant.', + partsCountTotal: 'Total {count}', + partsCountRunning: "{count} en cours d'exécution", + partsCountOff: '{count} désactivé', + partOff: 'Désactivé', + partsCountFailed: 'Échec du {count}', + partsFilter: 'Composants du filtre', + partsFilterEmpty: 'Aucun composant ne correspond.', + partToggle: 'Activer le composant {name}', + rowPhasePending: 'En attente de dépendances', + rowPhaseLoading: 'Chargement', + rowPhaseActive: 'Courir', + rowPhaseFailed: 'Problème', + rowPhaseUnloading: 'Déchargement', + enableToggle: 'Activer {name}', + applyingChange: 'Application des modifications à {name}', + openDetail: 'Voir {name}', + backToList: 'Retour aux plugins', + crumbRoot: 'Plugins', + backToPackage: 'Retour à {name}', + configureRow: 'Configurer {name}', + rowStateIdle: 'Ne fonctionne pas', + uninstall: 'Désinstaller', + uninstallLabel: 'Désinstaller {name}', + installTitle: 'Ajouter un plugin', + installDescription: "Entrez le nom du package du plugin, l'adresse du référentiel GitHub ou le chemin du répertoire local.", + installSpecLabel: 'Nom ou adresse du colis', + installSpecPlaceholder: 'par exemple dsh-plugin-whale-pet', + installGuideToggle: "Guide d'installation et exemples", + installGuideHide: 'Masquer le guide', + installGuideIdTitle: 'Entrez le nom du package npm du plugin', + installGuideIdExample: 'dsh-plugin-baleine-animal de compagnie', + installGuideIdHint: "Le nom du package du plugin est le nom du package npm (comme dsh-xxx ou @author/plugin) : la partie après dsh plugin add ou pnpm add dans la commande d'installation README d'un plugin communautaire.", + installGuideExampleLabel: 'Exemple : ', + installGitTemplateHint: "Remplacez-le par l'adresse réelle du référentiel Git.", + installPathTemplateHint: 'Remplacez-le par le chemin réel vers votre répertoire de plugin local.', + installGuideFill: "Utiliser l'exemple", + installGuideFillAria: "Utilisez l'exemple {example}", + installGuideSafety: "Installez uniquement les plugins en qui vous avez confiance : ils s'exécutent avec vos autorisations et peuvent endommager DeepSeek Harness ou divulguer vos données.", + installUpgradeNotice: "Les plugins officiels à la demande proposent la mise à jour lorsque leur version diffère de DSH. Pour mettre à jour d'autres plugins, désinstallez-les et réinstallez-les.", + registryToggle: 'Registre', + registryLegend: 'Le registre npm à partir duquel le plugin est téléchargé', + registryDefault: 'Registre par défaut', + registryOfficial: 'Registre officiel npm', + registryNpmmirror: 'Miroir de Chine continentale', + registryCustom: 'Adresse personnalisée', + registryCustomPlaceholder: 'https://npm.example.com/', + registryCustomHint: "Entrez une adresse de registre npm interne ou privée commençant par http:// ou https://.. Si cela nécessite une connexion, stockez les informations d'identification dans ~/.npmrc sur cette machine.", + registryCustomInvalid: 'Saisissez une adresse commençant par http:// ou https://', + registryListSeparator: ', ', + sentenceSeparator: ' ', + installRun: 'Installer', + installChecking: 'Vérification…', + installConfirmedTitle: 'Installer le plug-in', + installConfirmedRefused: 'Ce plugin ne peut pas être installé', + installProblemInvalid: "Il ne s'agit pas d'un nom ou d'une adresse de package pouvant être installé : {reason}", + installProblemInstalled: 'Ce plugin est déjà installé. Pour le mettre à niveau, désinstallez-le et réinstallez-le', + installProblemShipped: 'Ce plugin est livré avec DSH ; la mise à niveau de DSH le met à jour', + installBundledTitle: 'Ce plugin est livré avec DSH', + installBundledDescription: 'Activez-le sans télécharger une autre copie', + installProblemNotFound: "Aucun plugin de ce type n'a été trouvé", + installProblemNotPackage: "Le chemin n'existe pas ou n'est pas un package de plugin valide", + installProblemNotBundle: 'Ce package ne déclare aucun bundle, il ne peut donc pas être installé en tant que plugin : {reason}', + installProblemNetwork: "Le registre du plugin n'a pas pu être atteint ; vérifie le réseau et réessaye", + installProblemNetworkAll: "Aucun registre n'a pu être atteint (essayé : {registries}) ; vérifiez les paramètres réseau ou proxy, ou modifiez le registre", + installProblemUnknown: "Le plugin n'a pas pu être recherché : {reason}", + installingTitle: 'Installation du plugin…', + installedTitle: 'Installé', + statusNotInstalled: 'Non installé', + statusMissingFiles: 'Forfait indisponible', + statusUpdateAvailable: 'Mise à jour disponible', + sourceNotInstalled: 'Non installé', + sourceTargetSpec: 'Source pour ce DSH', + sourceTargetVersion: 'Version pour ce DSH', + update: 'Mise à jour', + updateLabel: 'Mettre à jour {name}', + versionsTitle: 'Versions', + versionsRecheck: 'Vérifiez à nouveau', + versionsChecking: 'Vérification des mises à jour', + versionsAvailable: 'Mise à jour vers {version} disponible', + versionsPublished: 'Publié {date}', + versionsLatest: 'À jour', + versionsCheckedAt: 'Dernière vérification à {time}', + versionsBlocked: 'Les versions plus récentes nécessitent une mise à jour DSH', + versionsBlockedDetail: '{version} a besoin de {peers} ; ce DSH est {runtime}', + versionsNoneInstallable: 'Aucune version plus récente à installer directement', + versionsNoneInstallableDetail: 'Les versions les plus récentes sont obsolètes, sont des versions préliminaires ou ne déclarent aucun bundle. Agrandissez pour les voir', + versionsFailedNetwork: "Impossible d'accéder au registre", + versionsFailedOther: 'Impossible de vérifier les mises à jour', + versionsDetails: 'Détails', + versionsNewerCount: '{count} plus récent', + versionsCollapse: 'Réduire', + versionsInstalledTag: 'Installé', + versionsPrereleaseTag: 'Avant-première', + versionsDeprecatedTag: 'Obsolète', + versionsNeedsDsh: 'Besoins {peers}', + versionsNotBundle: 'Cette version ne déclare aucun bundle et ne peut pas être installée', + versionsUpdateLabel: 'Mise à jour vers {version}', + versionsRegistry: 'Registre', + versionsCheckedAtLabel: 'Vérifié à', + versionsFallbackTag: 'Repli', + versionsFallbackHint: "{registry} n'a pas pu être atteint ; la liste provient d'un registre de secours", + versionsPrimaryTag: "D'abord", + versionsShowPrerelease: 'Afficher les avant-premières', + versionsOmitted: '{count} et les versions plus récentes que celle installée ne sont pas répertoriées', + versionsProblemNetwork: 'Injoignable', + versionsProblemNotFound: 'Pas trouvé', + versionsProblemUnknown: 'Échec', + updatesAvailable: 'Mises à jour disponibles : {count}', + updatesNone: 'Tous les plugins sont à jour', + updatesFailed: "Certains plugins n'ont pas pu être vérifiés", + updateTitle: 'Mettre à jour {name}', + updateSource: 'Installer à partir de', + updateDescription: "La mise à jour remplace {from} par {to} ; le plugin peut s'arrêter brièvement. Si l'installation échoue, {from} est conservé ou réinstallé.", + updateRunning: 'Mise à jour', + updateStepChecking: 'Vérifiez {version}', + updateStepRemoving: 'Arrêtez et supprimez {version}', + updateStepInstalling: 'Installez {version} à partir de {registry}', + updateStepRestoring: 'Installez à nouveau {version}', + updateDone: 'Mis à jour vers {version}', + updateDoneDetail: "La nouvelle version est en cours d'exécution.", + updateRestart: '{version} installé ; cela prend effet après un redémarrage', + updateRefused: 'Impossible de mettre à jour vers {version}', + updateRemoveFailed: 'Impossible de supprimer {version}', + updateRestored: 'La mise à jour a échoué ; restauré {version}', + updateRestoredDetail: "Échec de l'installation de {to} ; {from} a été réinstallé.", + updateKept: 'Impossible de mettre à jour vers {version}', + updateKeptDetail: '{from} n’a pas été supprimé et reste utilisé. {reason}', + updateBroken: "La mise à jour a échoué ; le plugin n'est pas installé", + updateBrokenDetail: "L'installation de {to} a échoué et l'installation de {from} a également échoué. Installez à nouveau {name} à partir d'Ajouter un plugin.", + updateLost: 'Le résultat de la mise à jour est inconnu', + updateLostDetail: 'La connexion à DSH a été interrompue : {reason}. Actualisez la liste des plugins pour voir la version installée.', + updateOutput: "Afficher le résultat de l'installation", + updateFinish: 'Terminé', + installFailedTitle: "Le plugin n'a pas pu être installé", + installGithubFailedTitle: "Impossible d'accéder à GitHub", + installGithubTimeoutTitle: 'La connexion GitHub a expiré', + installGithubFailedDescription: "Essayez une autre source d'installation.", + installUseGithubMirror: 'Utiliser un miroir de Chine continentale', + installTryAnotherWay: 'Essayez une autre façon', + installPackageLabel: 'Nom du package du plugin', + installEdit: 'Modifier', + installEditAria: "Retour à l'édition", + installCancelAndEdit: "Annuler l'installation et revenir à l'édition", + installApplyingCancellationError: "L'annulation n'a pas été confirmée. L'installation est en cours d'application ; attendre son résultat. {reason}", + installReconcile: "Vérifier l'état de l'installation", + installUnknownTitle: "Résultat de l'installation indisponible", + installUnknownDescription: "L'hôte n'a aucune installation active avec cet identifiant de demande. Vérifiez la liste des plugins avant de réessayer.", + installResultUnconfirmed: "Le résultat de l'installation n'a pas été reçu. Vérifiez l'état de l'installation. {reason}", + installAwaitingAcceptance: "En attente que l'hôte accepte l'installation. L'annulation sera réessayée automatiquement après confirmation.", + installBackgroundUnknown: "Résultat de l'installation indisponible. Vérifiez la liste des plugins.", + installCancel: "Annuler l'installation", + installCloseCancels: "Annuler l'installation et fermer", + installViewTask: "Voir l'installation", + installUnconfirmedTitle: "État de l'installation non confirmé", + installBackgroundDone: "Installation terminée. Afficher les détails de l'installation.", + installBackgroundFailed: "L'installation a échoué. Afficher les détails de l'installation.", + installBackgroundUnconfirmed: "L'état de l'installation n'est pas confirmé. Consultez l'installation pour plus de détails.", + installBackgroundApplying: "L'installation est en cours d'application et ne peut pas être annulée. Afficher la progression de l'installation.", + installStarting: "Préparation de l'installation…", + installCancelling: "Arrêt de l'installation…", + installApplying: 'Application de la configuration, veuillez patienter…', + installCancelledShort: 'Annulé', + installCancelled: "Installation annulée ; le plugin n'est pas activé et les fichiers téléchargés peuvent rester", + installCancelUnconfirmed: "L'installation n'a pas été confirmée arrêtée. Réessayez l'annulation ou attendez le résultat de l'installation. {reason}", + installEnableNow: 'Activer maintenant', + installDetailsShow: "Afficher les détails de l'installation", + installDetailsHide: "Masquer les détails de l'installation", + installVersion: 'Version {version}', + installSubjectPath: 'Annuaire local', + installSubjectGit: 'Dépôt Git', + installSubjectTarball: 'Tarball', + installLocation: "S'installe dans {dir}", + installRetry: 'Réessayer', + installChangeRegistry: 'Changer de registre', + installAttempt: "{previous} n'a pas pu servir le package ; nouvelle tentative via {registry} (registre {index} de {total})", + installAttemptBadge: 'Tentative {index} · {registry}', + installFailureNetwork: 'La connexion réseau a échoué', + installFailureNetworkAll: "Aucun registre n'a pu être atteint (essayé : {registries}). Vérifiez les paramètres réseau ou proxy, ou modifiez le registre et réessayez.", + installFailureNetworkHost: "{host} n'a pas pu être atteint. Une adresse GitHub ou un lien .tgz ne sont pas récupérés via le registre : cette machine doit y accéder directement ou via un proxy. Si le plugin est également publié sur npm, saisissez plutôt le nom de son package.", + installFailureNotFound: "Aucun plugin de ce type n'a été trouvé", + installFailureNoMatchingVersion: 'Aucune version ne correspond à la demande', + installFailureDiskFull: "Le disque est plein ; l'installation s'est arrêtée", + installFailurePermission: "Aucune autorisation d'écriture ; le plugin ne peut pas être installé", + installFailureBuildBlocked: "Les scripts d'installation d'une dépendance ont besoin de votre autorisation avant que l'installation puisse continuer", + installFailureBuildBlockedManual: "pnpm a bloqué les scripts d'installation ; autorisez-les sous AllowBuilds dans pnpm-workspace.yaml et réessayez", + installFailureIntegrity: "Le package téléchargé a échoué à sa vérification d'intégrité", + installFailureTimeout: "L'installation a expiré", + installFailurePnpmMissing: "pnpm n'a pas été trouvé, donc rien ne peut être installé", + installFailureGeneric: "Quelque chose s'est mal passé lors de l'installation ; les détails disent quoi", + terminalCommandLine: 'Ligne de commande {n}', + terminalRunning: 'Courir', + terminalFailed: 'Échec', + terminalDone: 'Terminé', + terminalCopy: 'Copier', + terminalCopied: 'Copié', + terminalNoOutput: 'Aucune sortie', + terminalCollapseAria: 'Réduire la sortie', + terminalCollapse: 'Réduire', + terminalExpandAria: 'Développez les lignes de sortie {n} restantes', + terminalExpand: '… {n} plus de lignes', + terminalExitCode: 'code de sortie {code}', + terminalSignal: 'signal {signal}', + terminalNoExitCode: 'pas de code de sortie', + installDoneNothing: 'Installation terminée sans nouvelle dépendance.', + installDoneRestart: 'Installé ; redémarrez DSH pour le charger. {reason}', + installDoneApproved: "Scripts d'installation autorisés pour {names}", + installDoneOtherVersion: "Conformément à la politique de sécurité de la chaîne d'approvisionnement de pnpm (minimumReleaseAge), les versions publiées plus récemment que l'âge requis ne sont pas installées automatiquement. Cette installation a donc utilisé {installed} au lieu de la dernière version {version}. Une version nouvellement publiée n'a pas dépassé cette période d'attente et peut contenir du code malveillant ou des défauts non détectés. Si vous avez besoin de {version}, désinstallez la version actuelle, puis installez {exact} pour demander cette version.", + installApprovalTitle: "Les scripts d'installation nécessitent une autorisation", + installApprovalDescription: "Ces packages contiennent des scripts d'installation que pnpm n'a pas exécutés.", + installApprovalConsequence: "Une fois autorisés, les scripts s'exécutent ici avec vos autorisations et l'autorisation est enregistrée dans ce profil.", + installApprovalCaution: 'Autorisez uniquement les packages auxquels vous faites confiance.', + installApproveAndRetry: 'Autoriser ces scripts et réessayer', + installClose: 'Terminé', + close: 'Fermer', + cancel: 'Annuler', + confirmUninstallTitle: 'Désinstaller "{name}" ?', + confirmUninstallDescription: "Ce qu'il fournit disparaît une fois désinstallé.", + confirmUninstall: 'Désinstaller', + failedEnable: "Impossible d'activer : {reason}", + failedDisable: 'Impossible de désactiver : {reason}', + failedUninstall: 'Impossible de désinstaller : {reason}', + failedRowEnable: "Impossible d'activer le composant : {reason}", + failedRowDisable: 'Impossible de désactiver le composant : {reason}', + reasonManagementRequired: 'La gestion des plugins en a besoin ; il ne peut pas être éteint ou désinstallé.', + reasonUnaddressable: 'Le correctif de profil ne peut pas résoudre ce problème de manière unique.', + reasonUnknownPlugin: 'Aucun plugin de ce type.', + reasonInvalidSpec: 'Entrez un nom ou une adresse de package valide.', + reasonAlreadyInstalled: "Ce package est déjà installé ; désinstallez-le avant d'installer une autre version.", + reasonAmbiguousInstall: 'Le changement de dépendance ne permet pas de déterminer quel package a été installé.', + reasonNotBundle: 'Ce package ne déclare aucun bundle, il ne peut donc pas être géré comme un plugin.', + reasonNotRemovable: "Ce package n'appartient pas au profil, ou la gestion des plugins en a besoin.", + reasonStopProfile: "Ce profil s'exécute sans HMR ; arrêtez-le et désinstallez le package avec le plugin dsh.", + reasonBundleInUse: "Une autre configuration utilise toujours les composants de ce bundle ; éteignez-les d'abord.", + reasonStaleApproval: 'Les approbations de script en attente ont changé ; réinstallez-les pour les actualiser.', + reasonIncompatibleVersion: '{plugin} est incompatible avec DSH {runtime} (nécessite {requirements}) ; son exécution peut provoquer des plantages ou une perte de données.', + reasonIncompatibleVersionUnnamed: "Ce plugin est incompatible avec la version DSH en cours d'exécution ; son exécution peut provoquer des plantages ou une perte de données.", + reasonIncompatibleInstall: 'Installez une version du plugin compatible avec ce DSH.', + reasonIncompatibleInstalled: 'Désinstallez-le et installez une version compatible avec ce DSH.', + reasonOperationError: "L'hôte a signalé une erreur.", + detailTitle: 'Détails', + detailLoading: 'Détails de lecture', + detailTooLarge: "Les détails dépassent la limite de taille et n'ont pas été chargés.", + detailNetwork: "Impossible d'atteindre la source des détails.", + detailInvalid: 'Le plugin déclare un fichier de détails invalide.', + detailFailed: 'Impossible de lire les détails.', + detailFootnotes: 'Notes de bas de page', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + panel: 'Plugins', + title: 'Plugins', + intro: 'Plugins installieren, aktivieren und konfigurieren', + infoLabel: 'Über Plugins', + infoDescription: 'Konfigurieren Sie hier offizielle Plugins und installieren oder verwalten Sie andere Plugins. Sehen Sie sich die Liste der integrierten Plugins und den Laufzeitstatus unter Einstellungen → Integrierte Plugins an.', + loading: 'Plugins lesen…', + error: 'Es konnten nicht alle Plugins gelesen werden, möglicherweise aufgrund eines Netzwerkproblems', + unavailable: 'Diese Bereitstellung läuft ohne verwaltbares Profil, daher können hier keine Plugins installiert oder gewechselt werden.', + retry: 'Erneut versuchen', + refresh: 'Aktualisieren', + refreshError: 'Aktualisierung fehlgeschlagen. Bitte versuchen Sie es erneut.', + empty: 'Es sind noch keine Plugins installiert.', + addPlugin: 'Plugin hinzufügen', + chooseAddMethod: 'Wählen Sie aus, wie Sie ein Plugin hinzufügen möchten', + installExisting: 'Installieren Sie ein Drittanbieter-Plugin', + installExistingDescription: 'Unterstützt npm-Pakete, Git-Repositorys und lokale Verzeichnisse', + restartNotice: 'Starten Sie DSH neu, um die Änderung zu übernehmen. {reason}', + restartReasonNoHmr: 'Dieses Profil wendet Änderungen nur beim Start an.', + restartReasonPackageMoved: 'Ein installiertes Paket wird nun in ein anderes Verzeichnis aufgelöst, oder die Änderung ersetzt ein Paket, das die DSH-Installation bereitstellt. Deinstallieren Sie ein Paket, bevor Sie es von einem anderen Speicherort installieren.', + restartReasonHostModule: 'Die Änderung erreicht die geladenen Module DSH selbst.', + restartReasonUnmanagedPlugin: 'Die Änderung betrifft ein Plugin, das außerhalb der Plugin-Liste ausgeführt wird.', + restartReasonModuleComparison: 'DSH konnte die geladenen Plugins nicht mit ihren Dateien vergleichen.', + overriddenNotice: '{name} wurde gespeichert, aber eine Konfiguration mit höherer Priorität überschreibt es, sodass es nicht wirksam ist', + bundlesTitle: 'Installierte Plugins', + basicTitle: 'Integrierte Plugin-Einstellungen', + extensionsTitle: 'Experimentelle Plugins', + moreTitle: 'Experimentelle Plugins', + moreAction: 'Mehr', + tabsLabel: 'Plugin-Ansichten', + installedTab: 'Installiert', + statusProblem: 'Problem', + statusBeta: 'Experimentell', + reasonLabel: 'Grund', + metadataError: 'Paketmetadatenfehler: {error}', + versionTag: 'v{version}', + partsLabel: 'Komponenten', + sourceTitle: 'Quelle', + sourceSpec: 'Codequelle', + sourceBuiltIn: 'Eingebaut', + sourceVersion: 'Aktuelle Version', + partsEmpty: 'Dieses Plugin-Paket enthält keine Komponenten.', + partsCountTotal: '{count} insgesamt', + partsCountRunning: '{count} läuft', + partsCountOff: '{count} aus', + partOff: 'Aus', + partsCountFailed: '{count} ist fehlgeschlagen', + partsFilter: 'Komponenten filtern', + partsFilterEmpty: 'Keine Komponentenübereinstimmungen.', + partToggle: 'Aktivieren Sie die Komponente {name}', + rowPhasePending: 'Warten auf Abhängigkeiten', + rowPhaseLoading: 'Laden', + rowPhaseActive: 'Laufen', + rowPhaseFailed: 'Problem', + rowPhaseUnloading: 'Entladen', + enableToggle: 'Aktivieren Sie {name}', + applyingChange: 'Anwenden von Änderungen auf {name}', + openDetail: 'Siehe {name}', + backToList: 'Zurück zu Plugins', + crumbRoot: 'Plugins', + backToPackage: 'Zurück zu {name}', + configureRow: 'Konfigurieren Sie {name}', + rowStateIdle: 'Läuft nicht', + uninstall: 'Deinstallieren', + uninstallLabel: 'Deinstallieren Sie {name}', + installTitle: 'Plugin hinzufügen', + installDescription: 'Geben Sie den Paketnamen des Plugins, die GitHub-Repository-Adresse oder den lokalen Verzeichnispfad ein.', + installSpecLabel: 'Name oder Adresse des Pakets', + installSpecPlaceholder: 'zum Beispiel dsh-plugin-whale-pet', + installGuideToggle: 'Installationsanleitung und Beispiele', + installGuideHide: 'Verstecke den Leitfaden', + installGuideIdTitle: 'Geben Sie den NPM-Paketnamen des Plugins ein', + installGuideIdExample: 'dsh-plugin-whale-pet', + installGuideIdHint: 'Der Plugin-Paketname ist der npm-Paketname (wie dsh-xxx oder @author/plugin): der Teil nach dshplugin add oder pnpm add im README-Installationsbefehl eines Community-Plugins.', + installGuideExampleLabel: 'Beispiel: ', + installGitTemplateHint: 'Ersetzen Sie dies durch die tatsächliche Git-Repository-Adresse.', + installPathTemplateHint: 'Ersetzen Sie dies durch den tatsächlichen Pfad zu Ihrem lokalen Plugin-Verzeichnis.', + installGuideFill: 'Beispiel verwenden', + installGuideFillAria: 'Verwenden Sie das Beispiel {example}', + installGuideSafety: 'Installieren Sie nur Plugins, denen Sie vertrauen: Sie werden mit Ihren Berechtigungen ausgeführt und können DeepSeek Harness beschädigen oder Ihre Daten preisgeben.', + installUpgradeNotice: 'Offizielle On-Demand-Plugins bieten Updates an, wenn ihre Version von DSH abweicht. Um andere Plugins zu aktualisieren, deinstallieren Sie sie und installieren Sie sie erneut.', + registryToggle: 'Registrierung', + registryLegend: 'Die NPM-Registrierung, aus der das Plugin heruntergeladen wird', + registryDefault: 'Standardregistrierung', + registryOfficial: 'Offizielle npm-Registrierung', + registryNpmmirror: 'Spiegel vom chinesischen Festland', + registryCustom: 'Benutzerdefinierte Adresse', + registryCustomPlaceholder: 'https://npm.example.com/', + registryCustomHint: 'Geben Sie eine interne oder private NPM-Registrierungsadresse ein, beginnend mit http:// oder https://.. Wenn eine Anmeldung erforderlich ist, speichern Sie die Anmeldeinformationen in ~/.npmrc auf diesem Computer.', + registryCustomInvalid: 'Geben Sie eine Adresse ein, die mit http:// oder https:// beginnt.', + registryListSeparator: ', ', + sentenceSeparator: ' ', + installRun: 'Installieren', + installChecking: 'Überprüfen…', + installConfirmedTitle: 'Plugin installieren', + installConfirmedRefused: 'Dieses Plugin kann nicht installiert werden', + installProblemInvalid: 'Dies ist kein Paketname oder keine Adresse, die installiert werden kann: {reason}', + installProblemInstalled: 'Dieses Plugin ist bereits installiert. Um es zu aktualisieren, deinstallieren Sie es und installieren Sie es erneut', + installProblemShipped: 'Dieses Plugin wird mit DSH geliefert; Durch das Aktualisieren von DSH wird es aktualisiert', + installBundledTitle: 'Dieses Plugin wird mit DSH geliefert', + installBundledDescription: 'Aktivieren Sie es, ohne eine weitere Kopie herunterzuladen', + installProblemNotFound: 'Es wurde kein solches Plugin gefunden', + installProblemNotPackage: 'Der Pfad existiert nicht oder ist kein gültiges Plugin-Paket', + installProblemNotBundle: 'Dieses Paket deklariert kein Bundle und kann daher nicht als Plugin installiert werden: {reason}', + installProblemNetwork: 'Die Plugin-Registrierung konnte nicht erreicht werden; Überprüfen Sie das Netzwerk und versuchen Sie es erneut', + installProblemNetworkAll: 'Es konnte keine Registry erreicht werden (versucht: {registries}); Überprüfen Sie die Netzwerk- oder Proxy-Einstellungen oder ändern Sie die Registrierung', + installProblemUnknown: 'Das Plugin konnte nicht gefunden werden: {reason}', + installingTitle: 'Plugin installieren…', + installedTitle: 'Installiert', + statusNotInstalled: 'Nicht installiert', + statusMissingFiles: 'Paket nicht verfügbar', + statusUpdateAvailable: 'Update verfügbar', + sourceNotInstalled: 'Nicht installiert', + sourceTargetSpec: 'Quelle hierfür DSH', + sourceTargetVersion: 'Version für dieses DSH', + update: 'Aktualisieren', + updateLabel: 'Aktualisieren Sie {name}', + versionsTitle: 'Versionen', + versionsRecheck: 'Überprüfen Sie es noch einmal', + versionsChecking: 'Suche nach Updates', + versionsAvailable: 'Update auf {version} verfügbar', + versionsPublished: 'Veröffentlicht {date}', + versionsLatest: 'Auf dem neuesten Stand', + versionsCheckedAt: 'Zuletzt überprüft bei {time}', + versionsBlocked: 'Neuere Versionen benötigen ein DSH-Update', + versionsBlockedDetail: '{version} benötigt {peers}; dieser DSH ist {runtime}', + versionsNoneInstallable: 'Keine neuere Version zur direkten Installation', + versionsNoneInstallableDetail: 'Die neueren Versionen sind veraltet, Vorabversionen oder deklarieren kein Bundle. Erweitern Sie, um sie zu sehen', + versionsFailedNetwork: 'Die Registrierung konnte nicht erreicht werden', + versionsFailedOther: 'Es konnte nicht nach Updates gesucht werden', + versionsDetails: 'Details', + versionsNewerCount: '{count} neuer', + versionsCollapse: 'Einklappen', + versionsInstalledTag: 'Installiert', + versionsPrereleaseTag: 'Vorabversion', + versionsDeprecatedTag: 'Veraltet', + versionsNeedsDsh: 'Benötigt {peers}', + versionsNotBundle: 'Diese Version deklariert kein Bundle und kann nicht installiert werden', + versionsUpdateLabel: 'Update auf {version}', + versionsRegistry: 'Registrierung', + versionsCheckedAtLabel: 'Geprüft bei', + versionsFallbackTag: 'Rückfall', + versionsFallbackHint: '{registry} konnte nicht erreicht werden; Die Liste stammt aus einer Fallback-Registrierung', + versionsPrimaryTag: 'Erstens', + versionsShowPrerelease: 'Vorabversionen anzeigen', + versionsOmitted: '{count} Weitere Versionen, die neuer als die installierte sind, werden nicht aufgeführt', + versionsProblemNetwork: 'Unerreichbar', + versionsProblemNotFound: 'Nicht gefunden', + versionsProblemUnknown: 'Fehlgeschlagen', + updatesAvailable: 'Verfügbare Updates: {count}', + updatesNone: 'Alle Plugins sind auf dem neuesten Stand', + updatesFailed: 'Einige Plugins konnten nicht überprüft werden', + updateTitle: 'Aktualisieren Sie {name}', + updateSource: 'Installieren von', + updateDescription: 'Das Update ersetzt {from} durch {to}; Das Plugin stoppt möglicherweise kurzzeitig. Wenn die Installation fehlschlägt, bleibt {from} erhalten oder wird erneut installiert.', + updateRunning: 'Aktualisierung', + updateStepChecking: 'Überprüfen Sie {version}', + updateStepRemoving: 'Stoppen und entfernen Sie {version}', + updateStepInstalling: 'Installieren Sie {version} von {registry}', + updateStepRestoring: 'Installieren Sie {version} erneut', + updateDone: 'Aktualisiert auf {version}', + updateDoneDetail: 'Die neue Version läuft.', + updateRestart: '{version} installiert; es wird erst nach einem Neustart wirksam', + updateRefused: 'Aktualisierung auf {version} nicht möglich', + updateRemoveFailed: '{version} konnte nicht entfernt werden', + updateRestored: 'Aktualisierung fehlgeschlagen; wiederhergestellt {version}', + updateRestoredDetail: '{to} konnte nicht installiert werden; {from} wurde erneut installiert.', + updateKept: 'Konnte nicht auf {version} aktualisiert werden', + updateKeptDetail: '{from} wurde nicht entfernt und wird weiterhin verwendet. {reason}', + updateBroken: 'Aktualisierung fehlgeschlagen; Das Plugin ist nicht installiert', + updateBrokenDetail: 'Die Installation von {to} ist fehlgeschlagen, und auch die erneute Installation von {from} schlug fehl. Installieren Sie {name} erneut über Plugin hinzufügen.', + updateLost: 'Das Update-Ergebnis ist unbekannt', + updateLostDetail: 'Die Verbindung zu DSH wurde unterbrochen: {reason}. Aktualisieren Sie die Plugin-Liste, um die installierte Version anzuzeigen.', + updateOutput: 'Installationsausgabe anzeigen', + updateFinish: 'Fertig', + installFailedTitle: 'Das Plugin konnte nicht installiert werden', + installGithubFailedTitle: 'Auf GitHub kann nicht zugegriffen werden', + installGithubTimeoutTitle: 'Zeitüberschreitung bei der GitHub-Verbindung', + installGithubFailedDescription: 'Versuchen Sie es mit einer anderen Installationsquelle.', + installUseGithubMirror: 'Verwenden Sie einen Spiegel vom chinesischen Festland', + installTryAnotherWay: 'Versuchen Sie es anders', + installPackageLabel: 'Name des Plugin-Pakets', + installEdit: 'Bearbeiten', + installEditAria: 'Zurück zur Bearbeitung', + installCancelAndEdit: 'Brechen Sie die Installation ab und kehren Sie zur Bearbeitung zurück', + installApplyingCancellationError: 'Die Stornierung wurde nicht bestätigt. Installation wird angewendet; warte auf das Ergebnis. {reason}', + installReconcile: 'Überprüfen Sie den Installationsstatus', + installUnknownTitle: 'Installationsergebnis nicht verfügbar', + installUnknownDescription: 'Der Host verfügt über keine aktive Installation mit dieser Anforderungs-ID. Überprüfen Sie die Plugin-Liste, bevor Sie es erneut versuchen.', + installResultUnconfirmed: 'Das Installationsergebnis wurde nicht empfangen. Überprüfen Sie den Installationsstatus. {reason}', + installAwaitingAcceptance: 'Warten darauf, dass der Host die Installation akzeptiert. Die Stornierung wird nach der Bestätigung automatisch wiederholt.', + installBackgroundUnknown: 'Installationsergebnis nicht verfügbar. Überprüfen Sie die Plugin-Liste.', + installCancel: 'Installation abbrechen', + installCloseCancels: 'Installation abbrechen und schließen', + installViewTask: 'Installation ansehen', + installUnconfirmedTitle: 'Installationsstatus unbestätigt', + installBackgroundDone: 'Installation abgeschlossen. Installationsdetails anzeigen.', + installBackgroundFailed: 'Die Installation ist fehlgeschlagen. Installationsdetails anzeigen.', + installBackgroundUnconfirmed: 'Der Installationsstatus ist unbestätigt. Weitere Informationen finden Sie in der Installation.', + installBackgroundApplying: 'Die Installation wird ausgeführt und kann nicht abgebrochen werden. Installationsfortschritt anzeigen.', + installStarting: 'Installation vorbereiten…', + installCancelling: 'Installation wird gestoppt…', + installApplying: 'Konfiguration wird angewendet, bitte warten…', + installCancelledShort: 'Abgesagt', + installCancelled: 'Installation abgebrochen; Das Plugin ist nicht aktiviert und heruntergeladene Dateien bleiben möglicherweise bestehen', + installCancelUnconfirmed: 'Die Installation wurde nicht bestätigt und gestoppt. Versuchen Sie den Abbruch erneut oder warten Sie auf das Installationsergebnis. {reason}', + installEnableNow: 'Jetzt aktivieren', + installDetailsShow: 'Installationsdetails anzeigen', + installDetailsHide: 'Installationsdetails ausblenden', + installVersion: 'Version {version}', + installSubjectPath: 'Lokales Verzeichnis', + installSubjectGit: 'Git-Repository', + installSubjectTarball: 'Tarball', + installLocation: 'Wird in {dir} installiert', + installRetry: 'Erneut versuchen', + installChangeRegistry: 'Registrierung ändern', + installAttempt: '{previous} konnte das Paket nicht bereitstellen; erneuter Versuch über {registry} (Registrierung {index} von {total})', + installAttemptBadge: 'Versuchen Sie {index} · {registry}', + installFailureNetwork: 'Die Netzwerkverbindung ist fehlgeschlagen', + installFailureNetworkAll: 'Es konnte keine Registry erreicht werden (versucht: {registries}). Überprüfen Sie die Netzwerk- oder Proxy-Einstellungen oder ändern Sie die Registrierung und versuchen Sie es erneut.', + installFailureNetworkHost: '{host} konnte nicht erreicht werden. Eine GitHub-Adresse oder ein .tgz-Link wird nicht über die Registrierung abgerufen: Dieser Computer muss sie direkt oder über einen Proxy erreichen. Wenn das Plugin auch auf npm veröffentlicht wird, geben Sie stattdessen seinen Paketnamen ein.', + installFailureNotFound: 'Es wurde kein solches Plugin gefunden', + installFailureNoMatchingVersion: 'Keine Version entspricht der Anfrage', + installFailureDiskFull: 'Die Festplatte ist voll; Die Installation wurde gestoppt', + installFailurePermission: 'Keine Schreibberechtigung; Das Plugin kann nicht installiert werden', + installFailureBuildBlocked: 'Die Installationsskripte einer Abhängigkeit benötigen Ihre Erlaubnis, bevor die Installation fortgesetzt werden kann', + installFailureBuildBlockedManual: 'pnpm blockierte Installationsskripte; Erlauben Sie sie unter „allowBuilds“ in „pnpm-workspace.yaml“ und versuchen Sie es erneut', + installFailureIntegrity: 'Das heruntergeladene Paket hat die Integritätsprüfung nicht bestanden', + installFailureTimeout: 'Bei der Installation ist eine Zeitüberschreitung aufgetreten', + installFailurePnpmMissing: 'pnpm wurde nicht gefunden, daher kann nichts installiert werden', + installFailureGeneric: 'Bei der Installation ist ein Fehler aufgetreten. Die Details sagen was', + terminalCommandLine: 'Befehlszeile {n}', + terminalRunning: 'Laufen', + terminalFailed: 'Fehlgeschlagen', + terminalDone: 'Fertig', + terminalCopy: 'Kopieren', + terminalCopied: 'Kopiert', + terminalNoOutput: 'Keine Ausgabe', + terminalCollapseAria: 'Ausgabe reduzieren', + terminalCollapse: 'Einklappen', + terminalExpandAria: 'Erweitern Sie die verbleibenden {n}-Ausgabezeilen', + terminalExpand: '… {n} weitere Zeilen', + terminalExitCode: 'Exit-Code {code}', + terminalSignal: 'Signal {signal}', + terminalNoExitCode: 'kein Exit-Code', + installDoneNothing: 'Die Installation ist ohne neue Abhängigkeit abgeschlossen.', + installDoneRestart: 'Installiert; Starten Sie DSH neu, um es zu laden. {reason}', + installDoneApproved: 'Für {names} zulässige Installationsskripte', + installDoneOtherVersion: 'Gemäß der Supply-Chain-Sicherheitsrichtlinie von pnpm (minimumReleaseAge) werden Versionen, die jünger als das erforderliche Alter veröffentlicht wurden, nicht automatisch installiert. Daher wurde bei dieser Installation {installed} anstelle der neuesten Version {version} verwendet. Eine neu veröffentlichte Version hat diese Wartezeit nicht durchlaufen und kann unerkannten Schadcode oder Mängel enthalten. Wenn Sie {version} benötigen, deinstallieren Sie die aktuelle Version und installieren Sie dann {exact}, um diese Version anzufordern.', + installApprovalTitle: 'Installationsskripte benötigen eine Genehmigung', + installApprovalDescription: 'Diese Pakete verfügen über Installationsskripte, die pnpm nicht ausgeführt hat.', + installApprovalConsequence: 'Sobald sie zugelassen sind, werden die Skripte hier mit Ihren Berechtigungen ausgeführt und die Berechtigung wird in diesem Profil gespeichert.', + installApprovalCaution: 'Lassen Sie nur Pakete zu, denen Sie vertrauen.', + installApproveAndRetry: 'Lassen Sie diese Skripte zu und versuchen Sie es erneut', + installClose: 'Fertig', + close: 'Schließen', + cancel: 'Abbrechen', + confirmUninstallTitle: '„{name}“ deinstallieren?', + confirmUninstallDescription: 'Was es bietet, verschwindet, sobald es deinstalliert wird.', + confirmUninstall: 'Deinstallieren', + failedEnable: 'Konnte nicht aktiviert werden: {reason}', + failedDisable: 'Konnte nicht deaktiviert werden: {reason}', + failedUninstall: 'Konnte nicht deinstalliert werden: {reason}', + failedRowEnable: 'Die Komponente {reason} konnte nicht aktiviert werden', + failedRowDisable: 'Die Komponente {reason} konnte nicht deaktiviert werden', + reasonManagementRequired: 'Das Plugin-Management braucht es; Es kann nicht ausgeschaltet oder deinstalliert werden.', + reasonUnaddressable: 'Der Profil-Patch kann dieses Problem nicht eindeutig beheben.', + reasonUnknownPlugin: 'Kein solches Plugin.', + reasonInvalidSpec: 'Geben Sie einen gültigen Paketnamen oder eine gültige Paketadresse ein.', + reasonAlreadyInstalled: 'Dieses Paket ist bereits installiert; Deinstallieren Sie es, bevor Sie eine andere Version installieren.', + reasonAmbiguousInstall: 'Welches Paket installiert wurde, lässt sich anhand der Abhängigkeitsänderung nicht erkennen.', + reasonNotBundle: 'Dieses Paket deklariert kein Bundle und kann daher nicht als Plugin verwaltet werden.', + reasonNotRemovable: 'Dieses Paket gehört nicht dem Profil oder die Plugin-Verwaltung benötigt es.', + reasonStopProfile: 'Dieses Profil läuft ohne HMR; Stoppen Sie es und deinstallieren Sie das Paket mit dem dsh-Plugin.', + reasonBundleInUse: 'Andere Konfigurationen verwenden weiterhin die Komponenten dieses Bundles; Schalten Sie sie zuerst aus.', + reasonStaleApproval: 'Die ausstehenden Drehbuchgenehmigungen haben sich geändert; Installieren Sie es erneut, um sie zu aktualisieren.', + reasonIncompatibleVersion: '{plugin} ist nicht kompatibel mit DSH {runtime} (erfordert {requirements}); Die Ausführung kann zu Abstürzen oder Datenverlust führen.', + reasonIncompatibleVersionUnnamed: 'Dieses Plugin ist nicht mit der laufenden DSH-Version kompatibel. Die Ausführung kann zu Abstürzen oder Datenverlust führen.', + reasonIncompatibleInstall: 'Installieren Sie eine mit diesem DSH kompatible Plugin-Version.', + reasonIncompatibleInstalled: 'Deinstallieren Sie es und installieren Sie eine mit diesem DSH kompatible Version.', + reasonOperationError: 'Der Host hat einen Fehler gemeldet.', + detailTitle: 'Details', + detailLoading: 'Details lesen', + detailTooLarge: 'Die Details überschreiten die Größenbeschränkung und wurden nicht geladen.', + detailNetwork: 'Die Quelle der Details konnte nicht erreicht werden.', + detailInvalid: 'Das Plugin deklariert eine ungültige Detaildatei.', + detailFailed: 'Die Details konnten nicht gelesen werden.', + detailFootnotes: 'Fußnoten', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + panel: 'Плагины', + title: 'Плагины', + intro: 'Установите, включите и настройте плагины', + infoLabel: 'О плагинах', + infoDescription: 'Настройте официальные плагины, а также установите или управляйте другими плагинами здесь. Посмотреть список встроенных плагинов и статус выполнения можно в разделе «Настройки» → «Встроенные плагины».', + loading: 'Чтение плагинов…', + error: 'Не удалось прочитать все плагины, возможно, из-за проблем с сетью.', + unavailable: 'Это развертывание выполняется без управляемого профиля, поэтому здесь нельзя устанавливать или переключать плагины.', + retry: 'Повторить', + refresh: 'Обновить', + refreshError: 'Обновить не удалось. Пожалуйста, попробуйте еще раз.', + empty: 'Плагины пока не установлены.', + addPlugin: 'Добавить плагин', + chooseAddMethod: 'Выберите, как добавить плагин', + installExisting: 'Установите сторонний плагин', + installExistingDescription: 'Поддерживает пакеты npm, репозитории Git и локальные каталоги.', + restartNotice: 'Перезапустите DSH, чтобы применить изменения. {reason}', + restartReasonNoHmr: 'Этот профиль применяет изменения только при запуске.', + restartReasonPackageMoved: 'Установленный пакет теперь разрешается в другой каталог, или изменение заменяет пакет, предоставляемый установкой DSH. Удалите пакет перед установкой его из другого места.', + restartReasonHostModule: 'Изменение доходит до загруженных модулей DSH.', + restartReasonUnmanagedPlugin: 'Изменение достигает плагина, работающего за пределами списка плагинов.', + restartReasonModuleComparison: 'DSH не смог сравнить загруженные плагины с их файлами.', + overriddenNotice: '{name} был сохранен, но конфигурация с более высоким приоритетом переопределяет его, поэтому он не действует.', + bundlesTitle: 'Установленные плагины', + basicTitle: 'Встроенные настройки плагина', + extensionsTitle: 'Экспериментальные плагины', + moreTitle: 'Экспериментальные плагины', + moreAction: 'Ещё', + tabsLabel: 'Просмотры плагинов', + installedTab: 'Установлено', + statusProblem: 'Проблема', + statusBeta: 'Экспериментальный', + reasonLabel: 'Причина', + metadataError: 'Ошибка метаданных пакета: {error}.', + versionTag: 'v{version}', + partsLabel: 'Компоненты', + sourceTitle: 'Источник', + sourceSpec: 'Источник кода', + sourceBuiltIn: 'Встроенный', + sourceVersion: 'Текущая версия', + partsEmpty: 'Этот пакет плагинов не содержит компонентов.', + partsCountTotal: '{count} всего', + partsCountRunning: '{count} работает', + partsCountOff: '{count} выкл.', + partOff: 'Выкл.', + partsCountFailed: '{count} не удалось', + partsFilter: 'Компоненты фильтра', + partsFilterEmpty: 'Ни один компонент не соответствует.', + partToggle: 'Включить компонент {name}', + rowPhasePending: 'Ожидание зависимостей', + rowPhaseLoading: 'Загрузка', + rowPhaseActive: 'Бег', + rowPhaseFailed: 'Проблема', + rowPhaseUnloading: 'Разгрузка', + enableToggle: 'Включить {name}', + applyingChange: 'Применение изменений в {name}', + openDetail: 'Посмотреть {name}', + backToList: 'Вернуться к плагинам', + crumbRoot: 'Плагины', + backToPackage: 'Вернуться к {name}', + configureRow: 'Настроить {name}', + rowStateIdle: 'Не работает', + uninstall: 'Удалить', + uninstallLabel: 'Удалить {name}', + installTitle: 'Добавить плагин', + installDescription: 'Введите имя пакета плагина, адрес репозитория GitHub или путь к локальному каталогу.', + installSpecLabel: 'Название или адрес пакета', + installSpecPlaceholder: 'например dsh-plugin-whale-pet', + installGuideToggle: 'Руководство по установке и примеры', + installGuideHide: 'Скрыть руководство', + installGuideIdTitle: 'Введите имя пакета npm плагина.', + installGuideIdExample: 'dsh-плагин-кит-питомец', + installGuideIdHint: 'Имя пакета плагина — это имя пакета npm (например, dsh-xxx или @author/plugin): часть после добавления плагина dsh или добавления pnpm в команде установки README плагина сообщества.', + installGuideExampleLabel: 'Пример: ', + installGitTemplateHint: 'Замените это фактическим адресом репозитория Git.', + installPathTemplateHint: 'Замените это фактическим путем к вашему локальному каталогу плагина.', + installGuideFill: 'Используйте пример', + installGuideFillAria: 'Используйте пример {example}', + installGuideSafety: 'Устанавливайте только те плагины, которым вы доверяете: они запускаются с вашими разрешениями и могут повредить DeepSeek Harness или привести к утечке ваших данных.', + installUpgradeNotice: 'Официальные плагины по требованию предлагают обновление, если их версия отличается от DSH. Чтобы обновить другие плагины, удалите и переустановите их.', + registryToggle: 'Реестр', + registryLegend: 'Реестр npm, из которого загружается плагин.', + registryDefault: 'Реестр по умолчанию', + registryOfficial: 'Официальный реестр npm', + registryNpmmirror: 'Зеркало материкового Китая', + registryCustom: 'Пользовательский адрес', + registryCustomPlaceholder: 'https://npm.example.com/', + registryCustomHint: 'Введите внутренний или частный адрес реестра npm, начинающийся с http:// или https://.. Если требуется вход в систему, сохраните учетные данные в ~/.npmrc на этом компьютере.', + registryCustomInvalid: 'Введите адрес, начинающийся с http:// или https://.', + registryListSeparator: ', ', + sentenceSeparator: ' ', + installRun: 'Установить', + installChecking: 'Проверка…', + installConfirmedTitle: 'Установить плагин', + installConfirmedRefused: 'Этот плагин не может быть установлен', + installProblemInvalid: 'Это не имя или адрес пакета, который можно установить: {reason}.', + installProblemInstalled: 'Этот плагин уже установлен. Чтобы обновить его, удалите его и установите снова.', + installProblemShipped: 'Этот плагин поставляется с DSH; обновление DSH обновляет его', + installBundledTitle: 'Этот плагин поставляется с DSH.', + installBundledDescription: 'Включите его, не загружая другую копию.', + installProblemNotFound: 'Такой плагин не найден', + installProblemNotPackage: 'Путь не существует или не является допустимым пакетом плагина.', + installProblemNotBundle: 'Этот пакет не декларирует отсутствие пакета, поэтому его нельзя установить как плагин: {reason}', + installProblemNetwork: 'Не удалось получить доступ к реестру плагинов; проверьте сеть и попробуйте еще раз', + installProblemNetworkAll: 'Не удалось получить доступ к реестру (пробовал: {registries}); проверьте настройки сети или прокси или измените реестр', + installProblemUnknown: 'Не удалось найти плагин: {reason}.', + installingTitle: 'Установка плагина…', + installedTitle: 'Установлено', + statusNotInstalled: 'Не установлено', + statusMissingFiles: 'Пакет недоступен', + statusUpdateAvailable: 'Доступно обновление', + sourceNotInstalled: 'Не установлено', + sourceTargetSpec: 'Источник для этого DSH', + sourceTargetVersion: 'Версия для этого DSH', + update: 'Обновить', + updateLabel: 'Обновление {name}', + versionsTitle: 'Версии', + versionsRecheck: 'Проверьте еще раз', + versionsChecking: 'Проверка обновлений', + versionsAvailable: 'Доступно обновление до {version}.', + versionsPublished: 'Опубликовано {date}', + versionsLatest: 'Актуально', + versionsCheckedAt: 'Последняя проверка на {time}.', + versionsBlocked: 'Для более новых версий требуется обновление DSH.', + versionsBlockedDetail: '{version} нужен {peers}; этот DSH - это {runtime}', + versionsNoneInstallable: 'Нет более новой версии для установки напрямую', + versionsNoneInstallableDetail: 'Более новые версии устарели, являются предварительными выпусками или не содержат пакетов. Разверните, чтобы увидеть их', + versionsFailedNetwork: 'Не удалось войти в реестр', + versionsFailedOther: 'Не удалось проверить наличие обновлений', + versionsDetails: 'Подробности', + versionsNewerCount: '{count} новее', + versionsCollapse: 'Свернуть', + versionsInstalledTag: 'Установлено', + versionsPrereleaseTag: 'Пререлиз', + versionsDeprecatedTag: 'Устарело', + versionsNeedsDsh: 'Требуется {peers}', + versionsNotBundle: 'Эта версия не декларирует отсутствие пакета и не может быть установлена.', + versionsUpdateLabel: 'Обновление до {version}', + versionsRegistry: 'Реестр', + versionsCheckedAtLabel: 'Проверено в', + versionsFallbackTag: 'Резервный вариант', + versionsFallbackHint: '{registry} не удалось связаться; список взят из резервного реестра', + versionsPrimaryTag: 'Первый', + versionsShowPrerelease: 'Показать пререлизы', + versionsOmitted: '{count} больше версий новее установленной не указано', + versionsProblemNetwork: 'недостижимый', + versionsProblemNotFound: 'Не найден', + versionsProblemUnknown: 'Не удалось', + updatesAvailable: 'Доступные обновления: {count}', + updatesNone: 'Все плагины актуальны', + updatesFailed: 'Некоторые плагины не удалось проверить', + updateTitle: 'Обновление {name}', + updateSource: 'Установить из', + updateDescription: 'Обновление заменяет {from} на {to}; плагин может ненадолго остановиться. Если установка не удалась, {from} сохраняется или устанавливается снова.', + updateRunning: 'Обновление', + updateStepChecking: 'Проверьте {version}', + updateStepRemoving: 'Остановите и удалите {version}.', + updateStepInstalling: 'Установите {version} из {registry}.', + updateStepRestoring: 'Установите {version} еще раз.', + updateDone: 'Обновлено до {version}.', + updateDoneDetail: 'Новая версия работает.', + updateRestart: '{version} установлен; оно вступит в силу после перезагрузки', + updateRefused: 'Не могу обновиться до {version}.', + updateRemoveFailed: 'Не удалось удалить {version}.', + updateRestored: 'Обновление не удалось; восстановлен {version}', + updateRestoredDetail: '{to} не удалось установить; {from} был установлен снова.', + updateKept: 'Не удалось обновиться до {version}.', + updateKeptDetail: '{from} не был удален и продолжает использоваться. {reason}', + updateBroken: 'Обновление не удалось; плагин не установлен', + updateBrokenDetail: '{to} не удалось установить, и повторная установка {from} также не удалась. Установите {name} снова из меню «Добавить плагин».', + updateLost: 'Результат обновления неизвестен', + updateLostDetail: 'Соединение с DSH было прервано: {reason}. Обновите список плагинов, чтобы увидеть установленную версию.', + updateOutput: 'Показать выходные данные установки', + updateFinish: 'Готово', + installFailedTitle: 'Плагин не удалось установить', + installGithubFailedTitle: 'Не могу получить доступ к GitHub', + installGithubTimeoutTitle: 'Время ожидания соединения GitHub истекло', + installGithubFailedDescription: 'Попробуйте другой источник установки.', + installUseGithubMirror: 'Используйте зеркало материкового Китая.', + installTryAnotherWay: 'Попробуйте другой способ', + installPackageLabel: 'Название пакета плагина', + installEdit: 'Редактировать', + installEditAria: 'Вернуться к редактированию', + installCancelAndEdit: 'Отменить установку и вернуться к редактированию', + installApplyingCancellationError: 'Отмена не подтвердилась. Установка применяется; дождитесь его результата. {reason}', + installReconcile: 'Проверьте статус установки', + installUnknownTitle: 'Результат установки недоступен', + installUnknownDescription: 'На хосте нет активной установки с этим идентификатором запроса. Прежде чем повторить попытку, проверьте список плагинов.', + installResultUnconfirmed: 'Результат установки не получен. Проверьте статус установки. {reason}', + installAwaitingAcceptance: 'Ожидание, пока хост примет установку. Отмена будет повторена автоматически после подтверждения.', + installBackgroundUnknown: 'Результат установки недоступен. Проверьте список плагинов.', + installCancel: 'Отменить установку', + installCloseCancels: 'Отменить установку и закрыть', + installViewTask: 'Посмотреть установку', + installUnconfirmedTitle: 'Статус установки не подтвержден', + installBackgroundDone: 'Установка завершена. Посмотреть подробности установки.', + installBackgroundFailed: 'Установка не удалась. Посмотреть подробности установки.', + installBackgroundUnconfirmed: 'Статус установки не подтвержден. Подробности смотрите в установке.', + installBackgroundApplying: 'Установка применяется и не может быть отменена. Просмотр хода установки.', + installStarting: 'Подготовка к установке…', + installCancelling: 'Остановка установки…', + installApplying: 'Применяется конфигурация, пожалуйста, подождите…', + installCancelledShort: 'Отменено', + installCancelled: 'Установка отменена; плагин не включен и скачанные файлы могут остаться', + installCancelUnconfirmed: 'Установка не подтверждена, остановлена. Повторите попытку отмены или дождитесь результата установки. {reason}', + installEnableNow: 'Включить сейчас', + installDetailsShow: 'Показать детали установки', + installDetailsHide: 'Скрыть сведения об установке', + installVersion: 'Версия {version}', + installSubjectPath: 'Локальный каталог', + installSubjectGit: 'Git-репозиторий', + installSubjectTarball: 'Тарбол', + installLocation: 'Устанавливается в {dir}', + installRetry: 'Повторить', + installChangeRegistry: 'Изменить реестр', + installAttempt: '{previous} не смог обслужить пакет; повторная попытка через {registry} (реестр {index} из {total})', + installAttemptBadge: 'Попытка {index} · {registry}', + installFailureNetwork: 'Сетевое подключение не удалось', + installFailureNetworkAll: 'Не удалось достичь реестра (пробовал: {registries}). Проверьте настройки сети или прокси-сервера или измените реестр и повторите попытку.', + installFailureNetworkHost: 'Не удалось связаться с {host}. Адрес GitHub или ссылка .tgz не извлекаются через реестр: этот компьютер должен получить доступ к ним напрямую или через прокси. Если плагин также опубликован в npm, вместо этого введите имя его пакета.', + installFailureNotFound: 'Такой плагин не найден', + installFailureNoMatchingVersion: 'Ни одна версия не соответствует запросу', + installFailureDiskFull: 'Диск заполнен; установка остановлена', + installFailurePermission: 'Нет разрешения на запись; плагин не может быть установлен', + installFailureBuildBlocked: 'Для продолжения установки сценариям установки зависимости требуется ваше разрешение.', + installFailureBuildBlockedManual: 'pnpm блокировал сценарии установки; разрешите их в разделеallowBuilds в pnpm-workspace.yaml и повторите попытку.', + installFailureIntegrity: 'Загруженный пакет не прошел проверку целостности.', + installFailureTimeout: 'Время установки истекло', + installFailurePnpmMissing: 'pnpm не найден, поэтому ничего установить невозможно', + installFailureGeneric: 'Во время установки что-то пошло не так; детали говорят о чем', + terminalCommandLine: 'Командная строка {n}', + terminalRunning: 'Бег', + terminalFailed: 'Не удалось', + terminalDone: 'Готово', + terminalCopy: 'Копировать', + terminalCopied: 'Скопировано', + terminalNoOutput: 'Нет вывода', + terminalCollapseAria: 'Свернуть вывод', + terminalCollapse: 'Свернуть', + terminalExpandAria: 'Разверните оставшиеся выходные строки {n}.', + terminalExpand: '… {n} еще строки', + terminalExitCode: 'код выхода {code}', + terminalSignal: 'сигнал {signal}', + terminalNoExitCode: 'нет кода выхода', + installDoneNothing: 'Установка завершена без новых зависимостей.', + installDoneRestart: 'Установлен; перезапустите DSH, чтобы загрузить его. {reason}', + installDoneApproved: 'Скрипты установки разрешены для {names}', + installDoneOtherVersion: 'В соответствии с политикой безопасности цепочки поставок pnpm (minimumReleaseAge) версии, опубликованные позже требуемого возраста, не устанавливаются автоматически, поэтому в этой установке использовалась {installed} вместо последней версии {version}. Недавно опубликованная версия не завершила этот период ожидания и может содержать необнаруженный вредоносный код или дефекты. Если вам нужен {version}, удалите текущую версию, а затем установите {exact}, чтобы запросить эту версию.', + installApprovalTitle: 'Для установки скриптов требуется разрешение', + installApprovalDescription: 'Эти пакеты содержат сценарии установки, которые pnpm не запускал.', + installApprovalConsequence: 'После разрешения сценарии запускаются здесь с вашими разрешениями, а разрешение сохраняется в этом профиле.', + installApprovalCaution: 'Разрешите только те пакеты, которым вы доверяете.', + installApproveAndRetry: 'Разрешите эти сценарии и повторите попытку.', + installClose: 'Готово', + close: 'Закрыть', + cancel: 'Отмена', + confirmUninstallTitle: 'Удалить "{name}"?', + confirmUninstallDescription: 'То, что он предоставляет, исчезает после его удаления.', + confirmUninstall: 'Удалить', + failedEnable: 'Не удалось включить: {reason}', + failedDisable: 'Не удалось отключить: {reason}', + failedUninstall: 'Не удалось удалить: {reason}', + failedRowEnable: 'Не удалось включить компонент: {reason}.', + failedRowDisable: 'Не удалось отключить компонент: {reason}.', + reasonManagementRequired: 'Управление плагинами нуждается в этом; его нельзя отключить или удалить.', + reasonUnaddressable: 'Патч профиля не может решить эту проблему однозначно.', + reasonUnknownPlugin: 'Нет такого плагина.', + reasonInvalidSpec: 'Введите действительное имя или адрес пакета.', + reasonAlreadyInstalled: 'Этот пакет уже установлен; удалите его перед установкой другой версии.', + reasonAmbiguousInstall: 'Какой пакет был установлен, невозможно определить по изменению зависимостей.', + reasonNotBundle: 'В этом пакете не объявлен пакет, поэтому им нельзя управлять как плагином.', + reasonNotRemovable: 'Этот пакет не принадлежит профилю, или он нужен управлению плагинами.', + reasonStopProfile: 'Этот профиль работает без HMR; остановите его и удалите пакет с плагином dsh.', + reasonBundleInUse: 'Другая конфигурация по-прежнему использует компоненты этого пакета; сначала выключите их.', + reasonStaleApproval: 'Ожидаемые утверждения сценария изменились; установите еще раз, чтобы обновить их.', + reasonIncompatibleVersion: '{plugin} несовместим с DSH {runtime} (требуется {requirements}); его запуск может привести к сбоям или потере данных.', + reasonIncompatibleVersionUnnamed: 'Этот плагин несовместим с работающей версией DSH; его запуск может привести к сбоям или потере данных.', + reasonIncompatibleInstall: 'Установите версию плагина, совместимую с этим DSH.', + reasonIncompatibleInstalled: 'Удалите его и установите версию, совместимую с этим DSH.', + reasonOperationError: 'Хост сообщил об ошибке.', + detailTitle: 'Подробности', + detailLoading: 'Читаем подробности', + detailTooLarge: 'Детали превышают лимит размера и не были загружены.', + detailNetwork: 'До источника подробностей добраться не удалось.', + detailInvalid: 'Плагин объявляет неверный файл сведений.', + detailFailed: 'Подробности прочитать не удалось.', + detailFootnotes: 'Сноски', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + panel: 'Plug-ins', + title: 'Plug-ins', + intro: 'Instalar, habilitar e configurar plug-ins', + infoLabel: 'Sobre plug-ins', + infoDescription: 'Configure plugins oficiais e instale ou gerencie outros plugins aqui. Veja a lista de plug-ins integrados e o status do tempo de execução em Configurações → Plug-ins integrados.', + loading: 'Lendo plug-ins…', + error: 'Não foi possível ler todos os plugins, possivelmente devido a um problema de rede', + unavailable: 'Esta implantação é executada sem um perfil gerenciável, portanto, os plug-ins não podem ser instalados ou trocados aqui.', + retry: 'Tentar novamente', + refresh: 'Atualizar', + refreshError: 'Falha na atualização. Por favor, tente novamente.', + empty: 'Nenhum plug-in está instalado ainda.', + addPlugin: 'Adicionar plug-in', + chooseAddMethod: 'Escolha como adicionar um plugin', + installExisting: 'Instale um plugin de terceiros', + installExistingDescription: 'Suporta pacotes npm, repositórios Git e diretórios locais', + restartNotice: 'Reinicie DSH para aplicar a alteração. {reason}', + restartReasonNoHmr: 'Este perfil aplica alterações somente na inicialização.', + restartReasonPackageMoved: 'Um pacote instalado agora é resolvido para outro diretório ou a alteração substitui um pacote fornecido pela instalação DSH. Desinstale um pacote antes de instalá-lo de outro local.', + restartReasonHostModule: 'A mudança atinge os módulos DSH carregados.', + restartReasonUnmanagedPlugin: 'A mudança atinge um plugin rodando fora da lista de plugins.', + restartReasonModuleComparison: 'DSH não conseguiu comparar os plug-ins carregados com seus arquivos.', + overriddenNotice: '{name} foi salvo, mas uma configuração de prioridade mais alta o substitui, portanto não está em vigor', + bundlesTitle: 'Plug-ins instalados', + basicTitle: 'Configurações de plug-in integradas', + extensionsTitle: 'Plug-ins experimentais', + moreTitle: 'Plug-ins experimentais', + moreAction: 'Mais', + tabsLabel: 'Visualizações de plug-in', + installedTab: 'Instalado', + statusProblem: 'Problema', + statusBeta: 'Experimental', + reasonLabel: 'Razão', + metadataError: 'Erro de metadados do pacote: {error}', + versionTag: 'v{version}', + partsLabel: 'Componentes', + sourceTitle: 'Fonte', + sourceSpec: 'Fonte do código', + sourceBuiltIn: 'Construído em', + sourceVersion: 'Versão atual', + partsEmpty: 'Este pacote de plugins não contém componentes.', + partsCountTotal: '{count} total', + partsCountRunning: '{count} em execução', + partsCountOff: '{count} desativado', + partOff: 'Desligado', + partsCountFailed: '{count} falhou', + partsFilter: 'Filtrar componentes', + partsFilterEmpty: 'Nenhum componente corresponde.', + partToggle: 'Ativar componente {name}', + rowPhasePending: 'Esperando por dependências', + rowPhaseLoading: 'Carregando', + rowPhaseActive: 'Correndo', + rowPhaseFailed: 'Problema', + rowPhaseUnloading: 'Descarregando', + enableToggle: 'Ativar {name}', + applyingChange: 'Aplicando alterações em {name}', + openDetail: 'Ver {name}', + backToList: 'Voltar aos plug-ins', + crumbRoot: 'Plug-ins', + backToPackage: 'Voltar para {name}', + configureRow: 'Configurar {name}', + rowStateIdle: 'Não está em execução', + uninstall: 'Desinstalar', + uninstallLabel: 'Desinstalar {name}', + installTitle: 'Adicionar plug-in', + installDescription: 'Insira o nome do pacote do plug-in, o endereço do repositório GitHub ou o caminho do diretório local.', + installSpecLabel: 'Nome ou endereço do pacote', + installSpecPlaceholder: 'por exemplo dsh-plugin-baleia-animal de estimação', + installGuideToggle: 'Guia de instalação e exemplos', + installGuideHide: 'Ocultar o guia', + installGuideIdTitle: 'Digite o nome do pacote npm do plugin', + installGuideIdExample: 'dsh-plugin-baleia-animal de estimação', + installGuideIdHint: 'O nome do pacote do plug-in é o nome do pacote npm (como dsh-xxx ou @author/plugin): a parte após dsh plugin add ou pnpm add no comando de instalação README de um plug-in da comunidade.', + installGuideExampleLabel: 'Exemplo: ', + installGitTemplateHint: 'Substitua pelo endereço real do repositório Git.', + installPathTemplateHint: 'Substitua isso pelo caminho real para o diretório local do plugin.', + installGuideFill: 'Exemplo de uso', + installGuideFillAria: 'Use o exemplo {example}', + installGuideSafety: 'Instale apenas plug-ins em que você confia: eles são executados com suas permissões e podem danificar DeepSeek Harness ou vazar seus dados.', + installUpgradeNotice: 'Plug-ins oficiais sob demanda oferecem atualização quando sua versão é diferente de DSH. Para atualizar outros plug-ins, desinstale-os e reinstale-os.', + registryToggle: 'Registro', + registryLegend: 'O registro npm do qual o plugin é baixado', + registryDefault: 'Registro padrão', + registryOfficial: 'Registro oficial do npm', + registryNpmmirror: 'Espelho da China Continental', + registryCustom: 'Endereço personalizado', + registryCustomPlaceholder: 'https://npm.example.com/', + registryCustomHint: 'Insira um endereço de registro npm interno ou privado começando com http:// ou https://. Se for necessário um login, armazene as credenciais em ~/.npmrc nesta máquina.', + registryCustomInvalid: 'Insira um endereço começando com http:// ou https://', + registryListSeparator: ', ', + sentenceSeparator: ' ', + installRun: 'Instalar', + installChecking: 'Verificando…', + installConfirmedTitle: 'Instalar plug-in', + installConfirmedRefused: 'Este plugin não pode ser instalado', + installProblemInvalid: 'Este não é um nome ou endereço de pacote que pode ser instalado: {reason}', + installProblemInstalled: 'Este plugin já está instalado. Para atualizá-lo, desinstale-o e instale-o novamente', + installProblemShipped: 'Este plug-in é fornecido com DSH; atualizar DSH atualiza-o', + installBundledTitle: 'Este plug-in é fornecido com DSH', + installBundledDescription: 'Habilite-o sem baixar outra cópia', + installProblemNotFound: 'Nenhum plugin desse tipo foi encontrado', + installProblemNotPackage: 'O caminho não existe ou não é um pacote de plugin válido', + installProblemNotBundle: 'Este pacote não declara nenhum pacote, portanto não pode ser instalado como um plug-in: {reason}', + installProblemNetwork: 'O registro do plugin não pôde ser acessado; verifique a rede e tente novamente', + installProblemNetworkAll: 'Nenhum registro pôde ser acessado (tentativa: {registries}); verifique as configurações de rede ou proxy ou altere o registro', + installProblemUnknown: 'Não foi possível pesquisar o plug-in: {reason}', + installingTitle: 'Instalando o plug-in…', + installedTitle: 'Instalado', + statusNotInstalled: 'Não instalado', + statusMissingFiles: 'Pacote indisponível', + statusUpdateAvailable: 'Atualização disponível', + sourceNotInstalled: 'Não instalado', + sourceTargetSpec: 'Fonte para este DSH', + sourceTargetVersion: 'Versão para este DSH', + update: 'Atualizar', + updateLabel: 'Atualizar {name}', + versionsTitle: 'Versões', + versionsRecheck: 'Verifique novamente', + versionsChecking: 'Verificando atualizações', + versionsAvailable: 'Atualização para {version} disponível', + versionsPublished: 'Publicado {date}', + versionsLatest: 'Atualizado', + versionsCheckedAt: 'Última verificação em {time}', + versionsBlocked: 'Versões mais recentes precisam de uma atualização DSH', + versionsBlockedDetail: '{version} precisa de {peers}; este DSH é {runtime}', + versionsNoneInstallable: 'Nenhuma versão mais recente para instalar diretamente', + versionsNoneInstallableDetail: 'As versões mais recentes estão obsoletas, são pré-lançamentos ou não declaram nenhum pacote. Expanda para vê-los', + versionsFailedNetwork: 'Não foi possível acessar o registro', + versionsFailedOther: 'Não foi possível verificar atualizações', + versionsDetails: 'Detalhes', + versionsNewerCount: '{count} mais recente', + versionsCollapse: 'Recolher', + versionsInstalledTag: 'Instalado', + versionsPrereleaseTag: 'Pré-lançamento', + versionsDeprecatedTag: 'Obsoleto', + versionsNeedsDsh: 'Precisa de {peers}', + versionsNotBundle: 'Esta versão não declara nenhum pacote e não pode ser instalada', + versionsUpdateLabel: 'Atualizar para {version}', + versionsRegistry: 'Registro', + versionsCheckedAtLabel: 'Verificado em', + versionsFallbackTag: 'Alternativa', + versionsFallbackHint: '{registry} não pôde ser contatado; a lista vem de um registro substituto', + versionsPrimaryTag: 'Primeiro', + versionsShowPrerelease: 'Mostrar pré-lançamentos', + versionsOmitted: '{count} mais versões mais recentes que a instalada não estão listadas', + versionsProblemNetwork: 'Inacessível', + versionsProblemNotFound: 'Não encontrado', + versionsProblemUnknown: 'Falha', + updatesAvailable: 'Atualizações disponíveis: {count}', + updatesNone: 'Todos os plug-ins estão atualizados', + updatesFailed: 'Alguns plug-ins não puderam ser verificados', + updateTitle: 'Atualizar {name}', + updateSource: 'Instalar de', + updateDescription: 'A atualização substitui {from} por {to}; o plugin pode parar brevemente. Se a instalação falhar, {from} será mantido ou instalado novamente.', + updateRunning: 'Atualizando', + updateStepChecking: 'Verifique {version}', + updateStepRemoving: 'Pare e remova {version}', + updateStepInstalling: 'Instalar {version} de {registry}', + updateStepRestoring: 'Instale {version} novamente', + updateDone: 'Atualizado para {version}', + updateDoneDetail: 'A nova versão está em execução.', + updateRestart: '{version} instalado; entra em vigor após uma reinicialização', + updateRefused: 'Não é possível atualizar para {version}', + updateRemoveFailed: 'Não foi possível remover {version}', + updateRestored: 'Falha na atualização; restaurado {version}', + updateRestoredDetail: '{to} falhou ao instalar; {from} foi instalado novamente.', + updateKept: 'Não foi possível atualizar para {version}', + updateKeptDetail: '{from} não foi removido e permanece em uso. {reason}', + updateBroken: 'Falha na atualização; o plugin não está instalado', + updateBrokenDetail: '{to} falhou ao instalar e instalar {from} novamente também falhou. Instale {name} novamente em Adicionar plug-in.', + updateLost: 'O resultado da atualização é desconhecido', + updateLostDetail: 'A conexão com DSH foi interrompida: {reason}. Atualize a lista de plugins para ver a versão instalada.', + updateOutput: 'Mostrar resultado da instalação', + updateFinish: 'Concluído', + installFailedTitle: 'O plugin não pôde ser instalado', + installGithubFailedTitle: 'Não é possível acessar GitHub', + installGithubTimeoutTitle: 'GitHub conexão expirou', + installGithubFailedDescription: 'Tente outra fonte de instalação.', + installUseGithubMirror: 'Use o espelho da China continental', + installTryAnotherWay: 'Tente outra maneira', + installPackageLabel: 'Nome do pacote de plug-ins', + installEdit: 'Editar', + installEditAria: 'Voltar para edição', + installCancelAndEdit: 'Cancele a instalação e retorne à edição', + installApplyingCancellationError: 'O cancelamento não foi confirmado. A instalação está sendo aplicada; espere pelo seu resultado. {reason}', + installReconcile: 'Verifique o status da instalação', + installUnknownTitle: 'Resultado da instalação indisponível', + installUnknownDescription: 'O Host não possui instalação ativa com este ID de solicitação. Verifique a lista de plugins antes de tentar novamente.', + installResultUnconfirmed: 'O resultado da instalação não foi recebido. Verifique o status da instalação. {reason}', + installAwaitingAcceptance: 'Aguardando o Host aceitar a instalação. O cancelamento será tentado novamente automaticamente após a confirmação.', + installBackgroundUnknown: 'Resultado da instalação indisponível. Verifique a lista de plugins.', + installCancel: 'Cancelar instalação', + installCloseCancels: 'Cancelar instalação e fechar', + installViewTask: 'Ver instalação', + installUnconfirmedTitle: 'Status de instalação não confirmado', + installBackgroundDone: 'Instalação concluída. Veja detalhes da instalação.', + installBackgroundFailed: 'A instalação falhou. Veja detalhes da instalação.', + installBackgroundUnconfirmed: 'O status da instalação não foi confirmado. Veja a instalação para obter detalhes.', + installBackgroundApplying: 'A instalação está sendo aplicada e não pode ser cancelada. Veja o progresso da instalação.', + installStarting: 'Preparando a instalação…', + installCancelling: 'Parando a instalação…', + installApplying: 'Aplicando configuração, aguarde…', + installCancelledShort: 'Cancelado', + installCancelled: 'Instalação cancelada; o plugin não está habilitado e os arquivos baixados podem permanecer', + installCancelUnconfirmed: 'A instalação não foi confirmada interrompida. Tente novamente o cancelamento ou aguarde o resultado da instalação. {reason}', + installEnableNow: 'Ativar agora', + installDetailsShow: 'Mostrar detalhes da instalação', + installDetailsHide: 'Ocultar detalhes de instalação', + installVersion: 'Versão {version}', + installSubjectPath: 'Diretório local', + installSubjectGit: 'Repositório Git', + installSubjectTarball: 'Tarball', + installLocation: 'Instala em {dir}', + installRetry: 'Tentar novamente', + installChangeRegistry: 'Alterar registro', + installAttempt: '{previous} não pôde entregar o pacote; tentando novamente através de {registry} (registro {index} de {total})', + installAttemptBadge: 'Tentativa {index} · {registry}', + installFailureNetwork: 'A conexão de rede falhou', + installFailureNetworkAll: 'Nenhum registro pôde ser acessado (tentativa: {registries}). Verifique as configurações de rede ou proxy ou altere o registro e tente novamente.', + installFailureNetworkHost: '{host} não pôde ser alcançado. Um endereço GitHub ou um link .tgz não é obtido através do registro: esta máquina deve acessá-lo diretamente ou através de um proxy. Se o plugin também for publicado no npm, insira o nome do pacote.', + installFailureNotFound: 'Nenhum plugin desse tipo foi encontrado', + installFailureNoMatchingVersion: 'Nenhuma versão corresponde à solicitação', + installFailureDiskFull: 'O disco está cheio; a instalação parou', + installFailurePermission: 'Sem permissão de gravação; o plugin não pode ser instalado', + installFailureBuildBlocked: 'Os scripts de instalação de uma dependência precisam da sua permissão antes que a instalação possa continuar', + installFailureBuildBlockedManual: 'scripts de instalação bloqueados por pnpm; permita-os em permitBuilds em pnpm-workspace.yaml e tente novamente', + installFailureIntegrity: 'O pacote baixado falhou na verificação de integridade', + installFailureTimeout: 'A instalação expirou', + installFailurePnpmMissing: 'pnpm não foi encontrado, então nada pode ser instalado', + installFailureGeneric: 'Algo deu errado durante a instalação; os detalhes dizem o que', + terminalCommandLine: 'Linha de comando {n}', + terminalRunning: 'Correndo', + terminalFailed: 'Falha', + terminalDone: 'Concluído', + terminalCopy: 'Copiar', + terminalCopied: 'Copiado', + terminalNoOutput: 'Sem saída', + terminalCollapseAria: 'Recolher saída', + terminalCollapse: 'Recolher', + terminalExpandAria: 'Expanda as {n} linhas de saída restantes', + terminalExpand: '… mais {n} linhas', + terminalExitCode: 'código de saída {code}', + terminalSignal: 'sinal {signal}', + terminalNoExitCode: 'sem código de saída', + installDoneNothing: 'A instalação foi concluída sem nenhuma nova dependência.', + installDoneRestart: 'Instalado; reinicie DSH para carregá-lo. {reason}', + installDoneApproved: 'Instalar scripts permitidos para {names}', + installDoneOtherVersion: 'De acordo com a política de segurança da cadeia de fornecimento do pnpm (minimumReleaseAge), as versões publicadas mais recentemente do que a idade exigida não são instaladas automaticamente, portanto, esta instalação usou {installed} em vez da versão mais recente {version}. Uma versão recém-publicada não completou esse período de espera e pode conter códigos maliciosos ou defeitos não detectados. Se precisar de {version}, desinstale a versão atual e instale {exact} para solicitar essa versão.', + installApprovalTitle: 'Os scripts de instalação precisam de permissão', + installApprovalDescription: 'Esses pacotes possuem scripts de instalação que o pnpm não executou.', + installApprovalConsequence: 'Uma vez permitido, os scripts são executados aqui com suas permissões, e a permissão é salva neste perfil.', + installApprovalCaution: 'Permita apenas pacotes em que você confia.', + installApproveAndRetry: 'Permita esses scripts e tente novamente', + installClose: 'Concluído', + close: 'Fechar', + cancel: 'Cancelar', + confirmUninstallTitle: 'Desinstalar "{name}"?', + confirmUninstallDescription: 'O que ele fornece desaparece quando é desinstalado.', + confirmUninstall: 'Desinstalar', + failedEnable: 'Não foi possível ativar: {reason}', + failedDisable: 'Não foi possível desativar: {reason}', + failedUninstall: 'Não foi possível desinstalar: {reason}', + failedRowEnable: 'Não foi possível ativar o componente: {reason}', + failedRowDisable: 'Não foi possível desativar o componente: {reason}', + reasonManagementRequired: 'O gerenciamento de plug-ins precisa disso; ele não pode ser desligado ou desinstalado.', + reasonUnaddressable: 'O patch de perfil não pode resolver este problema exclusivamente.', + reasonUnknownPlugin: 'Não existe tal plugin.', + reasonInvalidSpec: 'Insira um nome ou endereço de pacote válido.', + reasonAlreadyInstalled: 'Este pacote já está instalado; desinstale-o antes de instalar outra versão.', + reasonAmbiguousInstall: 'Qual pacote foi instalado não pode ser informado a partir da mudança de dependência.', + reasonNotBundle: 'Este pacote não declara nenhum pacote, portanto não pode ser gerenciado como um plugin.', + reasonNotRemovable: 'Este pacote não pertence ao perfil ou o gerenciamento do plugin precisa dele.', + reasonStopProfile: 'Este perfil é executado sem HMR; pare e desinstale o pacote com o plugin dsh.', + reasonBundleInUse: 'Outras configurações ainda usam os componentes deste pacote configurável; desligue-os primeiro.', + reasonStaleApproval: 'As aprovações de script pendentes foram alteradas; instale novamente para atualizá-los.', + reasonIncompatibleVersion: '{plugin} é incompatível com DSH {runtime} (requer {requirements}); executá-lo pode causar travamentos ou perda de dados.', + reasonIncompatibleVersionUnnamed: 'Este plug-in é incompatível com a versão DSH em execução; executá-lo pode causar travamentos ou perda de dados.', + reasonIncompatibleInstall: 'Instale uma versão de plug-in compatível com este DSH.', + reasonIncompatibleInstalled: 'Desinstale-o e instale uma versão compatível com este DSH.', + reasonOperationError: 'O Host relatou um erro.', + detailTitle: 'Detalhes', + detailLoading: 'Lendo detalhes', + detailTooLarge: 'Os detalhes excedem o limite de tamanho e não foram carregados.', + detailNetwork: 'Não foi possível acessar a fonte dos detalhes.', + detailInvalid: 'O plugin declara um arquivo de detalhes inválido.', + detailFailed: 'Não foi possível ler os detalhes.', + detailFootnotes: 'Notas de rodapé', +} satisfies Record diff --git a/packages/client/ui-plugin-market/README.i18n.yaml b/packages/client/ui-plugin-market/README.i18n.yaml index 4ca8e13747..1fa39181d9 100644 --- a/packages/client/ui-plugin-market/README.i18n.yaml +++ b/packages/client/ui-plugin-market/README.i18n.yaml @@ -3,23 +3,23 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-plugin-market/README.md /: - en: af3fe4b70bddbc1b - zh: ac1c4326165266d9 + en: b77d6d729562a88f + zh: b1634cc1d027424c /deepseek-ai-dsh-client-ui-plugin-market: en: 62972eb0dff8c145 zh: 682cc03b492cd730 /deepseek-ai-dsh-client-ui-plugin-market/summary: - en: 55a3d1b12cd7be61 - zh: a961cf0212b3fda8 + en: 71328d505b984fe5 + zh: 9281e7dbaa01ef9a /deepseek-ai-dsh-client-ui-plugin-market/table-of-contents: en: 76368b6b1da79c80 zh: 52384aee991ac542 /deepseek-ai-dsh-client-ui-plugin-market/use-this-package: - en: f873bf36195df821 - zh: e13c78ba3f3e29f1 + en: 868b100459e30aad + zh: 8a3a01199dc3c14c /deepseek-ai-dsh-client-ui-plugin-market/understand-the-implementation: - en: 4107a6967934f6b6 - zh: 46acec8bedb86329 + en: e1a480caaff8efea + zh: 0b725fa5bc929f22 /deepseek-ai-dsh-client-ui-plugin-market/model-experience: en: 08d25ab9cbdd7567 zh: 26e6e75c123951ee diff --git a/packages/client/ui-plugin-market/README.md b/packages/client/ui-plugin-market/README.md index 7a2bf9ffa8..283992a8c6 100644 --- a/packages/client/ui-plugin-market/README.md +++ b/packages/client/ui-plugin-market/README.md @@ -1,5 +1,5 @@ --- -description: "Discover tab of the Web Plugins page: search configured plugin market indexes, read an entry, and install it." +description: "Discover tab of the Web Plugins page: search configured plugin indexes, read an entry, and install it." kind: "package-reference" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Discover is the second tab of the **Plugins** page, after **Installed**. It lists the bundles that plugin market indexes offer; users manage index sources, switch between markets, search, filter by category and label, sort, open an entry's page, and install an entry after confirming it. Entry pages show each entry's long description as Markdown with images. The Host [`pluginMarket`](../../boot/plugin-market/README.md) service fetches and validates indexes; this package only presents them. +Discover is the second tab of the **Plugins** page, after **Installed**. It lists the bundles that plugin indexes offer; users manage index sources, switch between indexes, search, filter by category and label, sort, open an entry's page, and install an entry after confirming it. Entry pages show each entry's long description as Markdown with images. The Host [`pluginMarket`](../../boot/plugin-market/README.md) service fetches and validates indexes; this package only presents them. ## Table of Contents @@ -24,11 +24,11 @@ Discover is the second tab of the **Plugins** page, after **Installed**. It list ## Use this package -Open **Plugins** and select **Discover**. With no index source, the tab shows an empty state whose **Add index source** button opens the **Index sources** dialog. The dialog lists each source by market title, or by host before the first read, with its URL, the error of its last failed read, and a remove button, and keeps a notice that indexes come from third parties. A refused URL keeps the draft and says why: an unsupported scheme, credentials, or fragment; a URL already listed; or a deployment that cannot save sources. A newly added source becomes the selected market. +Open **Plugins** and select **Discover**. With no index source, the tab shows an empty state whose **Add index source** button opens the **Index sources** dialog. The dialog lists each source by index title, or by host before the first read, with its URL, the error of its last failed read, and a remove button, and keeps a notice that indexes come from third parties. A refused URL keeps the draft and says why: an unsupported scheme, credentials, or fragment; a URL already listed; or a deployment that cannot save sources. A newly added source becomes the selected index. -The toolbar holds the market menu, which switches markets and ends with **Manage index sources…**; the search field; a **Labels** menu with multi-select checkmarks when the market declares labels; the **Sort** menu; and a refresh button. Category pills, with **All** first, follow when the market declares categories. +The toolbar holds the index menu, which switches indexes and ends with **Manage index sources…**; the search field; a **Labels** menu with multi-select checkmarks when the index declares labels; the **Sort** menu; and a refresh button. Category pills, with **All** first, follow when the index declares categories. -Each card shows the icon, localized title, label tags, a one-line description, and the default version, author, and publish date, with one action: **Install** for an installable entry; a disabled **Install** whose tooltip says no version is compatible with the running DSH when the plugin has no default version and its highest version is neither compatible nor exempted; or **Installed**, **Update available**, or **Provided by DSH**, which switches to the Installed tab and opens that bundle's page when that tab lists it. Selecting a card opens the entry's page with its full description, compatibility notice, and, under the source heading, the default version's facts: author; homepage and source code repository, as links that open in a new tab, with the package directory beside the repository; the npm package and registry, or the Git repository, full commit, and directory; publish date; category; labels; and market. The page shows the long description first, without a heading of its own, then **Versions**, then the source facts. The **Versions** list is in descending SemVer order; each row tags the recommended, prerelease, installed, incompatible, and exempted versions and offers **Install** for any other version installation accepts, except for a bundle DSH provides. The page's back link returns to the list with its search, filters, and sort intact. +Each card shows the icon, localized title, label tags, a one-line description, and the default version, author, and publish date, with one action: **Install** for an installable entry; a disabled **Install** whose tooltip says no version is compatible with the running DSH when the plugin has no default version and its highest version is neither compatible nor exempted; or **Installed**, **Update available**, or **Provided by DSH**, which switches to the Installed tab and opens that bundle's page when that tab lists it. Selecting a card opens the entry's page with its full description, compatibility notice, and, under the source heading, the default version's facts: author; homepage and source code repository, as links that open in a new tab, with the package directory beside the repository; the npm package and registry, or the Git repository, full commit, and directory; publish date; category; labels; and index title. The page shows the long description first, without a heading of its own, then **Versions**, then the source facts. The **Versions** list is in descending SemVer order; each row tags the recommended, prerelease, installed, incompatible, and exempted versions and offers **Install** for any other version installation accepts, except for a bundle DSH provides. The page's back link returns to the list with its search, filters, and sort intact. **Install** chooses the default version and opens a confirmation dialog with the entry's icon, title, that version, the author, its source facts, an exemption notice when one applies, and a trust notice. Confirming opens the Plugins install dialog at its check screen with the entry's exact spec and registry; there is no spec form, and a refusal offers **Retry**. The registry an entry names applies to that install only. @@ -36,7 +36,7 @@ The Host [`pluginMarket.search`](../../boot/plugin-market/README.md#use-this-pac The description section shows the default version's long description in the active UI language: the version's locale `meta.detail` file, otherwise its package README, otherwise its repository README, in the [order `pluginMarket` documents](../../boot/plugin-market/README.md#plugin-detail-documents). While the document loads, the section shows three skeleton lines; an entry without a document shows no section; a failed read names its reason, such as a size limit, an unreachable source, or a refused repository, with **Retry**. Raw HTML is omitted, and a document taller than the section's height limit scrolls inside it. Relative image paths load from the Host beside the document, absolute HTTP(S) images load directly without a referrer, and relative links stay text. The section is the shared [`DetailDocument`](../ui-primitives/README.md#component-catalog) primitive. -Reading a market shows card skeletons. A failed read shows a short sentence with **Retry**, the Host's reason appears under the source in **Index sources**, and keeps the market's last listing; a failed read of the source list shows **Retry** in place of the tab. Discover re-reads sources on `plugin-market/changed` and connection reset, and recomputes installation status on `plugin-manager/changed`. +Reading an index shows card skeletons. A failed read shows a short sentence with **Retry**, the Host's reason appears under the source in **Index sources**, and keeps the index's last listing; a failed read of the source list shows **Retry** in place of the tab. Discover re-reads sources on `plugin-market/changed` and connection reset, and recomputes installation status on `plugin-manager/changed`. ----- @@ -45,7 +45,7 @@ Reading a market shows card skeletons. A failed read shows a short sentence with Detail reads are kept in a second snapshot store keyed by index URL, entry, version, and language; a section starts its read when it renders, an npm version's read first asks the Host's registry comparison and passes `npmFileSources: ['npmmirror']` when npmmirror answered fastest, only the latest read of a key settles it, **Retry** asks the Host to read again instead of returning its cached result, and a connection reset forgets every read, because a restarted Host no longer knows the document ids its images use, so each shown section reads its document again. Image URLs resolve `api/plugin-market/detail-file` against `document.baseURI`, so they keep a deployment prefix and use the Desktop `dsh-app://app/` origin, which [`MarkdownText`](../ui-primitives/README.md) accepts for that route. -The plugin registers the `discover` entry, labelled **Discover**, into `plugins.tab`, the tab slot that [`ui-plugin-manager`](../ui-plugin-manager/README.md) declares; the owner props provide `install({ spec, registry?, title, version })` and `openPackage(name)`; Discover passes the entry's localized title and the chosen version. A React-free controller owns source reads, source edits, and the selected index's query and loaded pages in a snapshot store; selecting another market starts from the initial query. Later pages send the first page's `fetchedAt`, and a page from a replaced document reloads the list once; a second replacement fails the read. The listing keeps the query its results answer, so the empty-state wording follows the shown results while a new query loads or after it fails, and **Retry** repeats the read that failed: the first pages of a query or the next page. A change to the profile's bundles reloads every loaded page to refresh installation status, and a filter or sort key the index no longer declares leaves the query. The unsent search text, the open entry, and the confirmation are component state. Pure helpers in `presentation.ts` derive the query order from the text, drop undeclared ids, order declarations with `Intl.Collator` in the active UI language, and list source facts. Icons load lazily with `referrerpolicy="no-referrer"` and fall back to the default artwork. +The plugin registers the `discover` entry, labelled **Discover**, into `plugins.tab`, the tab slot that [`ui-plugin-manager`](../ui-plugin-manager/README.md) declares; the owner props provide `install({ spec, registry?, title, version })` and `openPackage(name)`; Discover passes the entry's localized title and the chosen version. A React-free controller owns source reads, source edits, and the selected index's query and loaded pages in a snapshot store; selecting another index starts from the initial query. Later pages send the first page's `fetchedAt`, and a page from a replaced document reloads the list once; a second replacement fails the read. The listing keeps the query its results answer, so the empty-state wording follows the shown results while a new query loads or after it fails, and **Retry** repeats the read that failed: the first pages of a query or the next page. A change to the profile's bundles reloads every loaded page to refresh installation status, and a filter or sort key the index no longer declares leaves the query. The unsent search text, the open entry, and the confirmation are component state. Pure helpers in `presentation.ts` derive the query order from the text, drop undeclared ids, order declarations with `Intl.Collator` in the active UI language, and list source facts. Icons load lazily with `referrerpolicy="no-referrer"` and fall back to the default artwork. ----- diff --git a/packages/client/ui-plugin-market/README.zh.md b/packages/client/ui-plugin-market/README.zh.md index 40b98ec314..276c1a5556 100644 --- a/packages/client/ui-plugin-market/README.zh.md +++ b/packages/client/ui-plugin-market/README.zh.md @@ -1,5 +1,5 @@ --- -description: "Web 插件页的「发现」标签页:搜索已配置的插件市场索引,查看条目并安装。" +description: "Web 插件页的「发现」标签页:搜索已配置的插件索引,查看条目并安装。" kind: "package-reference" --- @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -「发现」是**插件**页的第二个标签页,位于**已安装**之后。它列出插件市场索引提供的组合包(bundle);用户可以管理索引源、在插件市场之间切换、搜索、按分类和标签筛选、排序、打开条目页面,并在确认后安装条目。条目页面以带图片的 Markdown 显示每个条目的详细介绍。Host 的 [`pluginMarket`](../../boot/plugin-market/README.zh.md) 服务负责获取和校验索引;本包只负责展示。 +「发现」是**插件**页的第二个标签页,位于**已安装**之后。它列出插件索引提供的组合包(bundle);用户可以管理索引源、在插件索引之间切换、搜索、按分类和标签筛选、排序、打开条目页面,并在确认后安装条目。条目页面以带图片的 Markdown 显示每个条目的详细介绍。Host 的 [`pluginMarket`](../../boot/plugin-market/README.zh.md) 服务负责获取和校验索引;本包只负责展示。 ## 目录 @@ -24,11 +24,11 @@ kind: "package-reference" ## 使用本包 -打开**插件**并选择**发现**。没有索引源时,该标签页显示空状态,其中的**添加索引源**按钮打开**索引源**对话框。对话框按插件市场标题列出每个索引源(首次读取前按主机名),并显示其 URL、最近一次读取失败的错误和删除按钮,同时保留一条提示,说明索引由第三方提供。URL 被拒绝时保留草稿并说明原因:不支持的协议、包含凭据或片段、URL 已在列表中,或当前部署无法保存索引源。新添加的索引源成为所选插件市场。 +打开**插件**并选择**发现**。没有索引源时,该标签页显示空状态,其中的**添加索引源**按钮打开**索引源**对话框。对话框按插件索引标题列出每个索引源(首次读取前按主机名),并显示其 URL、最近一次读取失败的错误和删除按钮,同时保留一条提示,说明索引由第三方提供。URL 被拒绝时保留草稿并说明原因:不支持的协议、包含凭据或片段、URL 已在列表中,或当前部署无法保存索引源。新添加的索引源成为所选插件索引。 -工具栏包含:插件市场菜单,用于切换插件市场,最后一项为**管理索引源…**;搜索框;插件市场声明了标签时显示的**标签**菜单,以勾选标记支持多选;**排序**菜单;以及刷新按钮。插件市场声明了分类时,其后显示分类按钮,**全部**排在第一位。 +工具栏包含:插件索引菜单,用于切换插件索引,最后一项为**管理索引源…**;搜索框;插件索引声明了标签时显示的**标签**菜单,以勾选标记支持多选;**排序**菜单;以及刷新按钮。插件索引声明了分类时,其后显示分类按钮,**全部**排在第一位。 -每张卡片显示图标、本地化标题、标签、一行描述,以及默认版本、作者和发布日期,并只有一个操作:可安装的条目显示**安装**;没有默认版本、且最高版本既不兼容也未豁免的条目显示禁用的**安装**,其提示框说明没有与当前运行的 DSH 兼容的版本;其余条目显示**已安装**、**有新版本**或**随 DSH 提供**,点击后切换到「已安装」标签页,并在该标签页列出该组合包时打开其页面。选择卡片会打开条目页面,显示其完整描述、兼容性提示,并在来源标题下显示默认版本的信息:作者;主页和源代码仓库(在新标签页中打开的链接,仓库旁显示包所在目录);npm 包和注册表,或 Git 仓库、完整提交和目录;发布日期;分类;标签;以及插件市场。页面先显示详细介绍,不带单独的标题,然后是**版本**,然后是来源信息。**版本**列表按 SemVer 降序排列;每行标出推荐、预发布、已安装、不兼容和已豁免的版本,并为安装检查接受的其他版本提供**安装**,DSH 随附的组合包除外。页面的返回链接回到列表,搜索、筛选和排序保持不变。 +每张卡片显示图标、本地化标题、标签、一行描述,以及默认版本、作者和发布日期,并只有一个操作:可安装的条目显示**安装**;没有默认版本、且最高版本既不兼容也未豁免的条目显示禁用的**安装**,其提示框说明没有与当前运行的 DSH 兼容的版本;其余条目显示**已安装**、**有新版本**或**随 DSH 提供**,点击后切换到「已安装」标签页,并在该标签页列出该组合包时打开其页面。选择卡片会打开条目页面,显示其完整描述、兼容性提示,并在来源标题下显示默认版本的信息:作者;主页和源代码仓库(在新标签页中打开的链接,仓库旁显示包所在目录);npm 包和注册表,或 Git 仓库、完整提交和目录;发布日期;分类;标签;以及插件索引标题。页面先显示详细介绍,不带单独的标题,然后是**版本**,然后是来源信息。**版本**列表按 SemVer 降序排列;每行标出推荐、预发布、已安装、不兼容和已豁免的版本,并为安装检查接受的其他版本提供**安装**,DSH 随附的组合包除外。页面的返回链接回到列表,搜索、筛选和排序保持不变。 **安装**选择默认版本并打开确认对话框,显示条目的图标、标题、该版本、作者、来源信息、适用时的豁免提示,以及信任提示。确认后,插件页的安装对话框以该条目的精确 spec 和注册表在检查界面打开;不显示 spec 表单,被拒绝时提供**重试**。条目指定的注册表只用于这一次安装。 @@ -36,7 +36,7 @@ Host 的 [`pluginMarket.search`](../../boot/plugin-market/README.zh.md#use-this- 详细介绍部分以当前界面语言显示默认版本的详细介绍:按 [`pluginMarket` 说明的顺序](../../boot/plugin-market/README.zh.md#plugin-detail-documents),依次为该版本 locale 中 `meta.detail` 指定的文件、其包 README、其仓库 README。文档加载期间,该部分显示三行骨架屏;没有文档的条目不显示该部分;读取失败时说明原因,例如超过大小限制、来源无法访问或仓库拒绝访问,并提供**重试**。原始 HTML 会被省略;文档高度超过该部分的高度上限时,在该部分内滚动。相对路径的图片从 Host 上该文档旁的位置加载,绝对 HTTP(S) 地址的图片不带 referrer 直接加载,相对链接保持为文本。该部分使用共享的 [`DetailDocument`](../ui-primitives/README.zh.md#component-catalog) 原语。 -读取插件市场时显示卡片骨架屏。读取失败时显示一句简短说明和**重试**,Host 给出的原因显示在**索引源**中对应索引源的下方,并保留该插件市场上一次的列表;读取索引源列表失败时,在该标签页的位置显示**重试**。「发现」在收到 `plugin-market/changed` 和连接重置时重新读取索引源,在收到 `plugin-manager/changed` 时重新计算安装状态。 +读取插件索引时显示卡片骨架屏。读取失败时显示一句简短说明和**重试**,Host 给出的原因显示在**索引源**中对应索引源的下方,并保留该插件索引上一次的列表;读取索引源列表失败时,在该标签页的位置显示**重试**。「发现」在收到 `plugin-market/changed` 和连接重置时重新读取索引源,在收到 `plugin-manager/changed` 时重新计算安装状态。 ----- @@ -45,7 +45,7 @@ Host 的 [`pluginMarket.search`](../../boot/plugin-market/README.zh.md#use-this- 详细介绍的读取结果保存在第二个快照存储中,以索引 URL、条目、版本和语言为键;该部分在渲染时开始读取;npm 版本的读取先询问 Host 的注册表比较,npmmirror 响应最快时传入 `npmFileSources: ['npmmirror']`;只有某个键最新一次的读取才会让该部分得到结果;**重试**要求 Host 重新读取,而不是返回其缓存的结果;连接重置时丢弃所有读取结果,因为重启后的 Host 不再知道其图片使用的文档 id,所以每个已显示的部分会重新读取其文档。图片 URL 基于 `document.baseURI` 解析 `api/plugin-market/detail-file`,因此保留部署前缀,并在 Desktop 中使用 `dsh-app://app/` origin;[`MarkdownText`](../ui-primitives/README.zh.md) 对该路由接受此 origin。 -本插件把标签为**发现**的 `discover` 条目注册到 `plugins.tab`,即 [`ui-plugin-manager`](../ui-plugin-manager/README.zh.md) 声明的标签页 slot;owner props 提供 `install({ spec, registry?, title, version })` 和 `openPackage(name)`;发现页传入条目的本地化标题和所选版本。一个不依赖 React 的控制器在快照存储中拥有索引源读取、索引源编辑,以及所选索引的查询和已加载的页;选择另一个插件市场时从初始查询开始。之后的页发送第一页的 `fetchedAt`,来自已替换文档的页会让列表重新加载一次;再次被替换时读取失败。列表保留其结果所回答的查询,因此新查询加载期间或失败后,空状态文案仍与显示的结果一致;**重试**重新执行失败的读取:某个查询的前几页,或下一页。profile 的组合包变化时,重新加载所有已加载的页以刷新安装状态;索引不再声明的过滤条件或排序键会从查询中移除。尚未发送的搜索文本、打开的条目和确认对话框是组件状态。`presentation.ts` 中的纯函数根据文本得出查询顺序、移除未声明的 id、使用当前界面语言的 `Intl.Collator` 对声明排序,以及列出来源信息。图标以 `referrerpolicy="no-referrer"` 延迟加载,失败时回退到默认插画。 +本插件把标签为**发现**的 `discover` 条目注册到 `plugins.tab`,即 [`ui-plugin-manager`](../ui-plugin-manager/README.zh.md) 声明的标签页 slot;owner props 提供 `install({ spec, registry?, title, version })` 和 `openPackage(name)`;发现页传入条目的本地化标题和所选版本。一个不依赖 React 的控制器在快照存储中拥有索引源读取、索引源编辑,以及所选索引的查询和已加载的页;选择另一个插件索引时从初始查询开始。之后的页发送第一页的 `fetchedAt`,来自已替换文档的页会让列表重新加载一次;再次被替换时读取失败。列表保留其结果所回答的查询,因此新查询加载期间或失败后,空状态文案仍与显示的结果一致;**重试**重新执行失败的读取:某个查询的前几页,或下一页。profile 的组合包变化时,重新加载所有已加载的页以刷新安装状态;索引不再声明的过滤条件或排序键会从查询中移除。尚未发送的搜索文本、打开的条目和确认对话框是组件状态。`presentation.ts` 中的纯函数根据文本得出查询顺序、移除未声明的 id、使用当前界面语言的 `Intl.Collator` 对声明排序,以及列出来源信息。图标以 `referrerpolicy="no-referrer"` 延迟加载,失败时回退到默认插画。 ----- diff --git a/packages/client/ui-plugin-market/locale/de.json b/packages/client/ui-plugin-market/locale/de.json new file mode 100644 index 0000000000..7575ee4faa --- /dev/null +++ b/packages/client/ui-plugin-market/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Plugin-Erkennung", + "description": "Konfigurierte Plugin-Indizes durchsuchen und deren Pakete installieren." + } +} diff --git a/packages/client/ui-plugin-market/locale/en.json b/packages/client/ui-plugin-market/locale/en.json index 2c72ab4cec..222f7277ed 100644 --- a/packages/client/ui-plugin-market/locale/en.json +++ b/packages/client/ui-plugin-market/locale/en.json @@ -1,6 +1,6 @@ { "meta": { "title": "Plugin discovery", - "description": "Browse configured plugin market indexes and install their packages." + "description": "Browse configured plugin indexes and install their packages." } } diff --git a/packages/client/ui-plugin-market/locale/es.json b/packages/client/ui-plugin-market/locale/es.json new file mode 100644 index 0000000000..a9dd1b071d --- /dev/null +++ b/packages/client/ui-plugin-market/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Descubrimiento de complementos", + "description": "Explore los índices de complementos configurados e instale sus paquetes." + } +} diff --git a/packages/client/ui-plugin-market/locale/fr.json b/packages/client/ui-plugin-market/locale/fr.json new file mode 100644 index 0000000000..e21adf5411 --- /dev/null +++ b/packages/client/ui-plugin-market/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Découverte de plugins", + "description": "Parcourez les index de plugins configurés et installez leurs paquets." + } +} diff --git a/packages/client/ui-plugin-market/locale/ja.json b/packages/client/ui-plugin-market/locale/ja.json index 7411851543..32e9d3c3d7 100644 --- a/packages/client/ui-plugin-market/locale/ja.json +++ b/packages/client/ui-plugin-market/locale/ja.json @@ -1,6 +1,6 @@ { "meta": { "title": "プラグイン検索 UI", - "description": "設定済みのプラグインマーケットのインデックスを参照し、プラグインパッケージをインストールします。" + "description": "設定済みのプラグインインデックスを参照し、プラグインパッケージをインストールします。" } } diff --git a/packages/client/ui-plugin-market/locale/pt.json b/packages/client/ui-plugin-market/locale/pt.json new file mode 100644 index 0000000000..6192b57fe6 --- /dev/null +++ b/packages/client/ui-plugin-market/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Descoberta de plug-ins", + "description": "Navegue pelos índices de plug-ins configurados e instale seus pacotes." + } +} diff --git a/packages/client/ui-plugin-market/locale/ru.json b/packages/client/ui-plugin-market/locale/ru.json new file mode 100644 index 0000000000..c990647d59 --- /dev/null +++ b/packages/client/ui-plugin-market/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Открытие плагина", + "description": "Просматривайте настроенные индексы плагинов и устанавливайте их пакеты." + } +} diff --git a/packages/client/ui-plugin-market/locale/zh.json b/packages/client/ui-plugin-market/locale/zh.json index 4e9e89725f..f01c89d5df 100644 --- a/packages/client/ui-plugin-market/locale/zh.json +++ b/packages/client/ui-plugin-market/locale/zh.json @@ -1,6 +1,6 @@ { "meta": { "title": "插件发现界面", - "description": "浏览已配置的插件市场索引,并安装其中的插件包。" + "description": "浏览已配置的插件索引,并安装其中的插件包。" } } diff --git a/packages/client/ui-plugin-market/src/client/index.ts b/packages/client/ui-plugin-market/src/client/index.ts index c1aab2c746..36aed35a01 100644 --- a/packages/client/ui-plugin-market/src/client/index.ts +++ b/packages/client/ui-plugin-market/src/client/index.ts @@ -12,7 +12,7 @@ import type {} from '@deepseek-ai/dsh-plugin-manager/types' import type {} from '@deepseek-ai/dsh-plugin-market/types' import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client' import { DiscoverSection } from './DiscoverSection.tsx' -import { en, ja, zh, type PluginMarketLocaleKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type PluginMarketLocaleKey } from './locales.ts' import { DiscoverController } from './market-store.ts' export type { DiscoverFace, DiscoverState } from './market-store.ts' @@ -36,7 +36,7 @@ export const inject = ['slots', 'locale', 'remote', 'remote.pluginMarket', 'remo * @param ctx - the browser plugin context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-plugin-market: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-plugin-market: dictionaries') const controller = new DiscoverController(ctx, () => document.baseURI) ctx.effect(() => () => { controller.dispose() }, 'ui-plugin-market: controller') ctx.effect(() => { diff --git a/packages/client/ui-plugin-market/src/client/locales.ts b/packages/client/ui-plugin-market/src/client/locales.ts index 20aada6cae..52753bd63a 100644 --- a/packages/client/ui-plugin-market/src/client/locales.ts +++ b/packages/client/ui-plugin-market/src/client/locales.ts @@ -8,14 +8,14 @@ export const zh = { retry: '重试', close: '关闭', cancel: '取消', - emptyTitle: '还没有插件市场', - emptyDescription: '添加一个插件市场索引,即可在这里浏览和安装其中的插件', + emptyTitle: '还没有插件索引', + emptyDescription: '添加一个插件索引,即可在这里浏览和安装其中的插件', addFirstSource: '添加索引源', sourcesFailed: '无法读取索引源', manageSources: '管理索引源…', - marketMenu: '切换插件市场,当前为 {market}', + marketMenu: '切换插件索引,当前为 {market}', sourcesTitle: '索引源', - sourcesDescription: '插件市场以一个索引文件的 URL 提供,「发现」从这里列出的索引浏览插件', + sourcesDescription: '插件索引以一个索引文件的 URL 提供,「发现」从这里列出的索引浏览插件', sourcesLabel: '已添加的索引源', sourceUrl: '索引 URL', sourcePlaceholder: 'https://example.com/index.json', @@ -70,7 +70,7 @@ export const zh = { factSourceCode: '源代码', factCategory: '分类', factLabels: '标签', - factMarket: '插件市场', + factMarket: '插件索引', versionsTitle: '版本', versionsLabel: '{name} 的版本', versionDefault: '推荐', @@ -102,14 +102,14 @@ export const en = { retry: 'Retry', close: 'Close', cancel: 'Cancel', - emptyTitle: 'No plugin markets yet', - emptyDescription: 'Add a plugin market index to browse and install its plugins here.', + emptyTitle: 'No plugin indexes yet', + emptyDescription: 'Add a plugin index to browse and install its plugins here.', addFirstSource: 'Add index source', sourcesFailed: 'Could not read the index sources.', manageSources: 'Manage index sources…', - marketMenu: 'Switch plugin market, currently {market}', + marketMenu: 'Switch plugin index, currently {market}', sourcesTitle: 'Index sources', - sourcesDescription: 'A plugin market is published as the URL of one index file. Discover browses the indexes listed here.', + sourcesDescription: 'A plugin index is published as the URL of one index file. Discover browses the indexes listed here.', sourcesLabel: 'Added index sources', sourceUrl: 'Index URL', sourcePlaceholder: 'https://example.com/index.json', @@ -164,7 +164,7 @@ export const en = { factSourceCode: 'Source code', factCategory: 'Category', factLabels: 'Labels', - factMarket: 'Plugin market', + factMarket: 'Plugin index', versionsTitle: 'Versions', versionsLabel: 'Versions of {name}', versionDefault: 'Recommended', @@ -193,14 +193,14 @@ export const ja = { 'retry': '再試行', 'close': '閉じる', 'cancel': 'キャンセル', - 'emptyTitle': 'プラグインマーケットはまだありません', - 'emptyDescription': 'プラグインマーケットのインデックスを追加すると、ここでプラグインを閲覧してインストールできます。', + 'emptyTitle': 'プラグインインデックスはまだありません', + 'emptyDescription': 'プラグインインデックスを追加すると、ここでそのプラグインを閲覧してインストールできます。', 'addFirstSource': 'インデックスソースを追加', 'sourcesFailed': 'インデックスソースを読み取れませんでした。', 'manageSources': 'インデックスソースを管理…', - 'marketMenu': 'プラグインマーケットを切り替え、現在は{market}', + 'marketMenu': 'プラグインインデックスを切り替え、現在は{market}', 'sourcesTitle': 'インデックスソース', - 'sourcesDescription': 'プラグインマーケットは1つのインデックスファイルのURLとして公開されます。「見つける」ではここに一覧されたインデックスを閲覧します。', + 'sourcesDescription': 'プラグインインデックスは1つのインデックスファイルのURLとして公開されます。「見つける」ではここに一覧されたインデックスを閲覧します。', 'sourcesLabel': '追加済みのインデックスソース', 'sourceUrl': 'インデックスURL', 'sourcePlaceholder': 'https://example.com/index.json', @@ -255,7 +255,7 @@ export const ja = { 'factSourceCode': 'ソースコード', 'factCategory': 'カテゴリ', 'factLabels': 'ラベル', - 'factMarket': 'プラグインマーケット', + 'factMarket': 'プラグインインデックス', 'versionsTitle': 'バージョン', 'versionsLabel': '{name} のバージョン', 'versionDefault': '推奨', @@ -275,3 +275,458 @@ export const ja = { 'detailFailed': '詳細を読み取れませんでした。', 'footnotes': '脚注', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'Descubrir', + refresh: 'Actualizar', + loading: 'leyendo el índice', + retry: 'Reintentar', + close: 'Cerrar', + cancel: 'Cancelar', + emptyTitle: 'Aún no hay índices de complementos', + emptyDescription: 'Agregue un índice de complementos para explorar e instalar sus complementos aquí.', + addFirstSource: 'Agregar fuente de índice', + sourcesFailed: 'No se pudieron leer las fuentes del índice.', + manageSources: 'Administrar fuentes de índice...', + marketMenu: 'Cambiar el índice de complementos; actual: {market}', + sourcesTitle: 'Fuentes del índice', + sourcesDescription: 'Un índice de complementos se publica mediante la URL de un archivo de índice. Descubrir explora los índices enumerados aquí.', + sourcesLabel: 'Fuentes de índice agregadas', + sourceUrl: 'URL del índice', + sourcePlaceholder: 'https://example.com/index.json', + addSource: 'Añadir', + removeSource: 'Quitar', + removeSourceNamed: 'Quitar {url}', + sourcesTrust: 'Los índices provienen de terceros. Agregue solo fuentes en las que confíe: sus complementos se ejecutan con sus permisos una vez instalados.', + sourceInvalid: 'Utilice una dirección http o https sin nombre de usuario, contraseña o fragmento #.', + sourceDuplicate: 'Esta fuente de índice ya está agregada.', + sourceUnavailable: 'Las fuentes del índice no se pueden guardar aquí.', + sourceFailed: 'No se pudieron guardar las fuentes del índice.', + readFailed: 'No se pudo leer este índice.', + readInvalid: 'Este índice no tiene un formato válido.', + readChanged: 'Este índice cambió durante la carga.', + search: 'Complementos de búsqueda', + filter: 'Etiquetas', + filterCount: 'Etiquetas · {count}', + sortBy: 'Tipo: {sort}', + sortRelevance: 'mejor partido', + sortDefault: 'Orden predeterminado', + sortPublished: 'Fecha de publicación', + switchToAscending: 'Orden ascendente', + switchToDescending: 'Orden descendente', + categoriesLabel: 'Categorías', + allCategories: 'Todos', + emptyIndex: 'Este índice no incluye complementos.', + noMatches: 'Ningún complemento coincide.', + showMore: 'Mostrar más', + entryMeta: '{version} · {date}', + entryMetaAuthor: '{version} · {author} · {date}', + openEntry: 'Ver {name}', + install: 'Instalar', + installTitle: 'Instalar {name}', + installVersionTitle: 'Instalar {name} {version}', + openInstalled: 'Ver {name} en Instalado', + statusInstalled: 'Instalado', + statusUpdate: 'Actualización disponible', + statusProvided: 'Proporcionado por DSH', + incompatible: 'Esta versión no es compatible con DSH {runtime} en ejecución; requiere DSH {range}.', + noCompatibleVersion: 'Ninguna versión es compatible con DSH {runtime}', + exempted: 'Esta versión no es compatible con DSH en ejecución, pero usted establece una exención de compatibilidad para ella.', + backToList: 'Descubrir', + factSource: 'Fuente', + factPackage: 'Paquete', + factRegistry: 'Registro', + factRepository: 'Repositorio', + factCommit: 'comprometerse', + factDirectory: 'Directorio', + factPublished: 'Publicado', + factAuthor: 'Autor', + factHomepage: 'Página de inicio', + factSourceCode: 'código fuente', + factCategory: 'Categoría', + factLabels: 'Etiquetas', + factMarket: 'Índice de complementos', + versionsTitle: 'Versiones', + versionsLabel: 'Versiones de {name}', + versionDefault: 'Recomendado', + versionInstalled: 'Instalado', + versionIncompatible: 'incompatible', + versionExempted: 'exento', + versionPrerelease: 'Presentación', + confirmTitle: '¿Instalar "{name}"?', + installTrust: 'Los complementos se ejecutan en esta máquina con sus permisos. Asegúrate de confiar en el origen de este.', + detailTitle: 'Detalles', + detailLoading: 'Detalles de lectura', + detailTooLarge: 'Los detalles o su repositorio exceden el límite de tamaño y no se cargaron.', + detailNetwork: 'No se pudo llegar a la fuente de los detalles.', + detailDenied: 'No se pudo acceder al repositorio de detalles. Verifique sus credenciales de Git.', + detailGitUnavailable: 'Los detalles están en un repositorio que debe leerse con Git y Git no se pudo ejecutar. Instale Git y vuelva a intentarlo.', + detailInvalid: 'El complemento declara un archivo de detalles no válido.', + detailFailed: 'No se pudieron leer los detalles.', + footnotes: 'Notas a pie de página', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: 'Découvrez', + refresh: 'Actualiser', + loading: "Lire l'index", + retry: 'Réessayer', + close: 'Fermer', + cancel: 'Annuler', + emptyTitle: 'Aucun index de plugins pour le moment', + emptyDescription: 'Ajoutez un index de plugins pour parcourir et installer ses plugins ici.', + addFirstSource: "Ajouter une source d'index", + sourcesFailed: "Impossible de lire les sources de l'index.", + manageSources: "Gérer les sources d'index…", + marketMenu: 'Changer d’index de plugins, actuellement {market}', + sourcesTitle: "Sources d'index", + sourcesDescription: 'Un index de plugins est publié à l’URL d’un fichier d’index. Découvrir parcourt les index répertoriés ici.', + sourcesLabel: "Sources d'index ajoutées", + sourceUrl: "URL de l'index", + sourcePlaceholder: 'https://example.com/index.json', + addSource: 'Ajouter', + removeSource: 'Supprimer', + removeSourceNamed: 'Supprimer {url}', + sourcesTrust: "Les index proviennent de tiers. Ajoutez uniquement des sources auxquelles vous faites confiance : leurs plugins s'exécutent avec vos autorisations une fois installés.", + sourceInvalid: "Utilisez une adresse http ou https sans nom d'utilisateur, mot de passe ou fragment #.", + sourceDuplicate: "Cette source d'index est déjà ajoutée.", + sourceUnavailable: "Les sources d'index ne peuvent pas être enregistrées ici.", + sourceFailed: "Impossible d'enregistrer les sources de l'index.", + readFailed: 'Impossible de lire cet index.', + readInvalid: "Cet index n'est pas dans un format valide.", + readChanged: 'Cet index a changé lors du chargement.', + search: 'Plugins de recherche', + filter: 'Étiquettes', + filterCount: 'Étiquettes · {count}', + sortBy: 'Trier : {sort}', + sortRelevance: 'Meilleure correspondance', + sortDefault: 'Ordre par défaut', + sortPublished: 'Date de publication', + switchToAscending: 'Trier par ordre croissant', + switchToDescending: 'Trier par ordre décroissant', + categoriesLabel: 'Catégories', + allCategories: 'Tout', + emptyIndex: 'Cet index ne répertorie aucun plugin.', + noMatches: 'Aucun plugin ne correspond.', + showMore: 'Afficher plus', + entryMeta: '{version} · {date}', + entryMetaAuthor: '{version} · {author} · {date}', + openEntry: 'Voir {name}', + install: 'Installer', + installTitle: 'Installer {name}', + installVersionTitle: 'Installer {name} {version}', + openInstalled: 'Afficher {name} dans Installé', + statusInstalled: 'Installé', + statusUpdate: 'Mise à jour disponible', + statusProvided: 'Fourni par DSH', + incompatible: "Cette version n'est pas compatible avec le DSH {runtime} en cours d'exécution ; cela nécessite DSH {range}.", + noCompatibleVersion: "Aucune version n'est compatible avec DSH {runtime}", + exempted: "Cette version n'est pas compatible avec le DSH en cours d'exécution, mais vous avez défini une exemption de compatibilité pour celui-ci.", + backToList: 'Découvrez', + factSource: 'Source', + factPackage: 'Forfait', + factRegistry: 'Registre', + factRepository: 'Référentiel', + factCommit: "S'engager", + factDirectory: 'Annuaire', + factPublished: 'Publié', + factAuthor: 'Auteur', + factHomepage: "Page d'accueil", + factSourceCode: 'Code source', + factCategory: 'Catégorie', + factLabels: 'Étiquettes', + factMarket: 'Index de plugins', + versionsTitle: 'Versions', + versionsLabel: 'Versions de {name}', + versionDefault: 'Recommandé', + versionInstalled: 'Installé', + versionIncompatible: 'Incompatible', + versionExempted: 'Exonéré', + versionPrerelease: 'Avant-première', + confirmTitle: 'Installer « {name} » ?', + installTrust: "Les plugins s'exécutent sur cette machine avec vos autorisations. Assurez-vous de faire confiance à l'origine de celui-ci.", + detailTitle: 'Détails', + detailLoading: 'Détails de lecture', + detailTooLarge: "Les détails ou leur référentiel dépassent la limite de taille et n'ont pas été chargés.", + detailNetwork: "Impossible d'atteindre la source des détails.", + detailDenied: "Impossible d'accéder au référentiel des détails. Vérifiez vos informations d'identification Git.", + detailGitUnavailable: "Les détails se trouvent dans un référentiel qui doit être lu avec Git, et Git n'a pas pu s'exécuter. Installez Git, puis réessayez.", + detailInvalid: 'Le plugin déclare un fichier de détails invalide.', + detailFailed: 'Impossible de lire les détails.', + footnotes: 'Notes de bas de page', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'Entdecken', + refresh: 'Aktualisieren', + loading: 'Den Index lesen', + retry: 'Erneut versuchen', + close: 'Schließen', + cancel: 'Abbrechen', + emptyTitle: 'Noch keine Plugin-Indizes', + emptyDescription: 'Einen Plugin-Index hinzufügen, um dessen Plugins hier zu durchsuchen und zu installieren.', + addFirstSource: 'Indexquelle hinzufügen', + sourcesFailed: 'Die Indexquellen konnten nicht gelesen werden.', + manageSources: 'Indexquellen verwalten…', + marketMenu: 'Plugin-Index wechseln, aktuell {market}', + sourcesTitle: 'Indexquellen', + sourcesDescription: 'Ein Plugin-Index wird über die URL einer Indexdatei veröffentlicht. Entdecken durchsucht die hier aufgeführten Indizes.', + sourcesLabel: 'Indexquellen hinzugefügt', + sourceUrl: 'Index-URL', + sourcePlaceholder: 'https://example.com/index.json', + addSource: 'Hinzufügen', + removeSource: 'Entfernen', + removeSourceNamed: 'Entfernen Sie {url}', + sourcesTrust: 'Indizes stammen von Dritten. Fügen Sie nur Quellen hinzu, denen Sie vertrauen: Ihre Plugins werden nach der Installation mit Ihren Berechtigungen ausgeführt.', + sourceInvalid: 'Verwenden Sie eine http- oder https-Adresse ohne Benutzernamen, Passwort oder #-Fragment.', + sourceDuplicate: 'Diese Indexquelle wurde bereits hinzugefügt.', + sourceUnavailable: 'Indexquellen können hier nicht gespeichert werden.', + sourceFailed: 'Die Indexquellen konnten nicht gespeichert werden.', + readFailed: 'Dieser Index konnte nicht gelesen werden.', + readInvalid: 'Dieser Index hat kein gültiges Format.', + readChanged: 'Dieser Index hat sich beim Laden geändert.', + search: 'Such-Plugins', + filter: 'Etiketten', + filterCount: 'Etiketten · {count}', + sortBy: 'Sortierung: {sort}', + sortRelevance: 'Beste Übereinstimmung', + sortDefault: 'Standardreihenfolge', + sortPublished: 'Veröffentlichungsdatum', + switchToAscending: 'Aufsteigend sortieren', + switchToDescending: 'Absteigend sortieren', + categoriesLabel: 'Kategorien', + allCategories: 'Alle', + emptyIndex: 'Dieser Index listet keine Plugins auf.', + noMatches: 'Es stimmen keine Plugins überein.', + showMore: 'Mehr anzeigen', + entryMeta: '{version} · {date}', + entryMetaAuthor: '{version} · {author} · {date}', + openEntry: 'Siehe {name}', + install: 'Installieren', + installTitle: 'Installieren Sie {name}', + installVersionTitle: 'Installieren Sie {name} {version}', + openInstalled: '{name} unter „Installiert“ anzeigen', + statusInstalled: 'Installiert', + statusUpdate: 'Update verfügbar', + statusProvided: 'Bereitgestellt von DSH', + incompatible: 'Diese Version ist nicht mit dem laufenden DSH {runtime} kompatibel; es erfordert DSH {range}.', + noCompatibleVersion: 'Keine Version ist mit DSH {runtime} kompatibel', + exempted: 'Diese Version ist nicht mit der laufenden Version DSH kompatibel, Sie haben jedoch eine Kompatibilitätsausnahme dafür festgelegt.', + backToList: 'Entdecken', + factSource: 'Quelle', + factPackage: 'Paket', + factRegistry: 'Registrierung', + factRepository: 'Repository', + factCommit: 'Begehen', + factDirectory: 'Verzeichnis', + factPublished: 'Veröffentlicht', + factAuthor: 'Autor', + factHomepage: 'Homepage', + factSourceCode: 'Quellcode', + factCategory: 'Kategorie', + factLabels: 'Etiketten', + factMarket: 'Plugin-Index', + versionsTitle: 'Versionen', + versionsLabel: 'Versionen von {name}', + versionDefault: 'Empfohlen', + versionInstalled: 'Installiert', + versionIncompatible: 'Inkompatibel', + versionExempted: 'Ausgenommen', + versionPrerelease: 'Vorabversion', + confirmTitle: '„{name}“ installieren?', + installTrust: 'Plugins werden mit Ihren Berechtigungen auf diesem Computer ausgeführt. Stellen Sie sicher, dass Sie darauf vertrauen, woher dieser kommt.', + detailTitle: 'Details', + detailLoading: 'Details lesen', + detailTooLarge: 'Die Details oder ihr Repository überschreiten die Größenbeschränkung und wurden nicht geladen.', + detailNetwork: 'Die Quelle der Details konnte nicht erreicht werden.', + detailDenied: 'Auf das Repository mit den Details konnte nicht zugegriffen werden. Überprüfen Sie Ihre Git-Anmeldeinformationen.', + detailGitUnavailable: 'Die Details befinden sich in einem Repository, das mit Git gelesen werden muss, und Git konnte nicht ausgeführt werden. Installieren Sie Git und versuchen Sie es erneut.', + detailInvalid: 'Das Plugin deklariert eine ungültige Detaildatei.', + detailFailed: 'Die Details konnten nicht gelesen werden.', + footnotes: 'Fußnoten', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'Откройте для себя', + refresh: 'Обновить', + loading: 'Чтение индекса', + retry: 'Повторить', + close: 'Закрыть', + cancel: 'Отмена', + emptyTitle: 'Индексы плагинов ещё не добавлены', + emptyDescription: 'Добавьте индекс плагинов, чтобы просматривать и устанавливать его плагины здесь.', + addFirstSource: 'Добавить источник индекса', + sourcesFailed: 'Не удалось прочитать исходники индекса.', + manageSources: 'Управление источниками индексов…', + marketMenu: 'Переключить индекс плагинов, текущий: {market}', + sourcesTitle: 'Источники индекса', + sourcesDescription: 'Индекс плагинов публикуется по URL файла индекса. В разделе «Обнаружение» можно просматривать перечисленные здесь индексы.', + sourcesLabel: 'Добавлены источники индекса', + sourceUrl: 'URL-адрес индекса', + sourcePlaceholder: 'https://example.com/index.json', + addSource: 'Добавить', + removeSource: 'Удалить', + removeSourceNamed: 'Удалить {url}', + sourcesTrust: 'Индексы поступают от третьих лиц. Добавляйте только те источники, которым вы доверяете: их плагины запускаются с вашими разрешениями после установки.', + sourceInvalid: 'Используйте адрес http или https без имени пользователя, пароля или фрагмента #.', + sourceDuplicate: 'Этот источник индекса уже добавлен.', + sourceUnavailable: 'Источники индекса здесь сохранить нельзя.', + sourceFailed: 'Не удалось сохранить источники индекса.', + readFailed: 'Не удалось прочитать этот индекс.', + readInvalid: 'Этот индекс имеет недопустимый формат.', + readChanged: 'Этот индекс менялся при загрузке.', + search: 'Поиск плагинов', + filter: 'Этикетки', + filterCount: 'Этикетки · {count}', + sortBy: 'Сортировать: {sort}', + sortRelevance: 'Лучший матч', + sortDefault: 'Порядок по умолчанию', + sortPublished: 'Дата публикации', + switchToAscending: 'Сортировать по возрастанию', + switchToDescending: 'Сортировать по убыванию', + categoriesLabel: 'Категории', + allCategories: 'Все', + emptyIndex: 'В этом индексе нет плагинов.', + noMatches: 'Никакие плагины не подходят.', + showMore: 'Показать больше', + entryMeta: '{version} · {date}', + entryMetaAuthor: '{version} · {author} · {date}', + openEntry: 'Посмотреть {name}', + install: 'Установить', + installTitle: 'Установить {name}', + installVersionTitle: 'Установить {name} {version}', + openInstalled: 'Посмотреть {name} в списке «Установлено»', + statusInstalled: 'Установлено', + statusUpdate: 'Доступно обновление', + statusProvided: 'Предоставлено DSH', + incompatible: 'Эта версия несовместима с работающим DSH {runtime}; для этого требуется DSH {range}.', + noCompatibleVersion: 'Ни одна версия не совместима с DSH {runtime}.', + exempted: 'Эта версия несовместима с работающим DSH, но вы установили для нее исключение совместимости.', + backToList: 'Откройте для себя', + factSource: 'Источник', + factPackage: 'Пакет', + factRegistry: 'Реестр', + factRepository: 'Репозиторий', + factCommit: 'Зафиксировать', + factDirectory: 'Каталог', + factPublished: 'Опубликовано', + factAuthor: 'Автор', + factHomepage: 'Домашняя страница', + factSourceCode: 'Исходный код', + factCategory: 'Категория', + factLabels: 'Этикетки', + factMarket: 'Индекс плагинов', + versionsTitle: 'Версии', + versionsLabel: 'Версии {name}', + versionDefault: 'Рекомендуется', + versionInstalled: 'Установлено', + versionIncompatible: 'Несовместимый', + versionExempted: 'освобожден', + versionPrerelease: 'Пререлиз', + confirmTitle: 'Установить "{name}"?', + installTrust: 'Плагины запускаются на этом компьютере с вашего разрешения. Убедитесь, что вы доверяете источнику этого сообщения.', + detailTitle: 'Подробности', + detailLoading: 'Читаем подробности', + detailTooLarge: 'Детали или их репозиторий превышают предельный размер и не были загружены.', + detailNetwork: 'До источника подробностей добраться не удалось.', + detailDenied: 'Не удалось получить доступ к хранилищу реквизитов. Проверьте свои учетные данные Git.', + detailGitUnavailable: 'Подробности находятся в репозитории, который необходимо прочитать с помощью Git, и Git не удалось запустить. Установите Git и повторите попытку.', + detailInvalid: 'Плагин объявляет неверный файл сведений.', + detailFailed: 'Подробности прочитать не удалось.', + footnotes: 'Сноски', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'Descubra', + refresh: 'Atualizar', + loading: 'Lendo o índice', + retry: 'Tentar novamente', + close: 'Fechar', + cancel: 'Cancelar', + emptyTitle: 'Ainda não há índices de plug-ins', + emptyDescription: 'Adicione um índice de plug-ins para navegar e instalar seus plug-ins aqui.', + addFirstSource: 'Adicionar fonte de índice', + sourcesFailed: 'Não foi possível ler as fontes do índice.', + manageSources: 'Gerenciar fontes de índice…', + marketMenu: 'Mudar de índice de plug-ins, atual: {market}', + sourcesTitle: 'Fontes de índice', + sourcesDescription: 'Um índice de plug-ins é publicado pela URL de um arquivo de índice. Descobrir navega pelos índices listados aqui.', + sourcesLabel: 'Fontes de índice adicionadas', + sourceUrl: 'URL do índice', + sourcePlaceholder: 'https://example.com/index.json', + addSource: 'Adicionar', + removeSource: 'Remover', + removeSourceNamed: 'Remover {url}', + sourcesTrust: 'Os índices vêm de terceiros. Adicione apenas fontes em que você confia: seus plug-ins são executados com suas permissões, uma vez instalados.', + sourceInvalid: 'Use um endereço http ou https sem nome de usuário, senha ou fragmento #.', + sourceDuplicate: 'Esta fonte de índice já foi adicionada.', + sourceUnavailable: 'As fontes do índice não podem ser salvas aqui.', + sourceFailed: 'Não foi possível salvar as fontes do índice.', + readFailed: 'Não foi possível ler este índice.', + readInvalid: 'Este índice não está em um formato válido.', + readChanged: 'Este índice mudou durante o carregamento.', + search: 'Plug-ins de pesquisa', + filter: 'Etiquetas', + filterCount: 'Marcadores · {count}', + sortBy: 'Classificar: {sort}', + sortRelevance: 'Melhor correspondência', + sortDefault: 'Ordem padrão', + sortPublished: 'Data de publicação', + switchToAscending: 'Classificar em ordem crescente', + switchToDescending: 'Classificar em ordem decrescente', + categoriesLabel: 'Categorias', + allCategories: 'Todos', + emptyIndex: 'Este índice não lista plug-ins.', + noMatches: 'Nenhum plug-in corresponde.', + showMore: 'Mostrar mais', + entryMeta: '{version} · {date}', + entryMetaAuthor: '{version} · {author} · {date}', + openEntry: 'Ver {name}', + install: 'Instalar', + installTitle: 'Instalar {name}', + installVersionTitle: 'Instalar {name} {version}', + openInstalled: 'Ver {name} em Instalado', + statusInstalled: 'Instalado', + statusUpdate: 'Atualização disponível', + statusProvided: 'Fornecido por DSH', + incompatible: 'Esta versão não é compatível com o DSH {runtime} em execução; requer DSH {range}.', + noCompatibleVersion: 'Nenhuma versão é compatível com DSH {runtime}', + exempted: 'Esta versão não é compatível com o DSH em execução, mas você definiu uma isenção de compatibilidade para ela.', + backToList: 'Descubra', + factSource: 'Fonte', + factPackage: 'Pacote', + factRegistry: 'Registro', + factRepository: 'Repositório', + factCommit: 'Confirmar', + factDirectory: 'Diretório', + factPublished: 'Publicado', + factAuthor: 'Autor', + factHomepage: 'Página inicial', + factSourceCode: 'Código fonte', + factCategory: 'Categoria', + factLabels: 'Etiquetas', + factMarket: 'Índice de plug-ins', + versionsTitle: 'Versões', + versionsLabel: 'Versões de {name}', + versionDefault: 'Recomendado', + versionInstalled: 'Instalado', + versionIncompatible: 'Incompatível', + versionExempted: 'Isento', + versionPrerelease: 'Pré-lançamento', + confirmTitle: 'Instalar "{name}"?', + installTrust: 'Os plug-ins são executados nesta máquina com suas permissões. Certifique-se de confiar de onde este vem.', + detailTitle: 'Detalhes', + detailLoading: 'Lendo detalhes', + detailTooLarge: 'Os detalhes ou seu repositório excedem o limite de tamanho e não foram carregados.', + detailNetwork: 'Não foi possível acessar a fonte dos detalhes.', + detailDenied: 'Não foi possível acessar o repositório dos detalhes. Verifique suas credenciais do Git.', + detailGitUnavailable: 'Os detalhes estão em um repositório que deve ser lido com o Git, e o Git não pôde ser executado. Instale o Git e tente novamente.', + detailInvalid: 'O plugin declara um arquivo de detalhes inválido.', + detailFailed: 'Não foi possível ler os detalhes.', + footnotes: 'Notas de rodapé', +} satisfies Record diff --git a/packages/client/ui-plugin-market/tests/discover.client.spec.tsx b/packages/client/ui-plugin-market/tests/discover.client.spec.tsx index 88ee3e6cd3..965275e9f8 100644 --- a/packages/client/ui-plugin-market/tests/discover.client.spec.tsx +++ b/packages/client/ui-plugin-market/tests/discover.client.spec.tsx @@ -50,7 +50,7 @@ const LISTING: MarketSearchResult = { fetchedAt: 1, total: 6, nextOffset: null, - title: { en: 'Alpha market', zh: '甲市场' }, + title: { en: 'Alpha index', zh: '甲索引' }, metadata: { categories: { tools: { title: 'Tools' }, themes: { title: 'Themes' } }, labels: { official: { title: 'Official' }, beta: { title: 'Beta' } }, @@ -231,7 +231,7 @@ describe('DiscoverSection', () => { expect(screen.getByText(en.emptyIndex)).toBeTruthy() }) - it('sorts by publish date, a market sort key, either direction, and back to the index order', () => { + it('sorts by publish date, an index sort key, either direction, and back to the index order', () => { const { actions, answer } = renderTab() openMenu(/^Sort: Default order/) pick(en.sortPublished) @@ -256,7 +256,7 @@ describe('DiscoverSection', () => { answer(INITIAL_QUERY) openMenu(/^Sort: Default order/) expect(screen.queryByRole('menuitem', { name: en.switchToAscending })).toBeNull() - for (const name of [/^Sort:/, en.filter, /^Switch plugin market/]) { + for (const name of [/^Sort:/, en.filter, /^Switch plugin index/]) { fireEvent.keyDown(document.activeElement ?? document.body, { key: 'Escape' }) expect(screen.queryByRole('menu')).toBeNull() openMenu(name) @@ -264,17 +264,17 @@ describe('DiscoverSection', () => { } fireEvent.keyDown(document.activeElement ?? document.body, { key: 'Escape' }) expect(screen.queryByRole('menu')).toBeNull() - openMenu(/^Switch plugin market/) - openMenu(/^Switch plugin market/) + openMenu(/^Switch plugin index/) + openMenu(/^Switch plugin index/) expect(screen.queryByRole('menu')).toBeNull() }) - it('switches markets and opens source management from the market menu', () => { + it('switches indexes and opens source management from the index menu', () => { const { actions } = renderTab() - openMenu('Switch plugin market, currently Alpha market') + openMenu('Switch plugin index, currently Alpha index') pick('b.example') expect(actions.select).toHaveBeenCalledWith(B) - openMenu(/^Switch plugin market/) + openMenu(/^Switch plugin index/) pick(en.manageSources) expect(actions.toggleSources).toHaveBeenCalledOnce() }) @@ -287,7 +287,7 @@ describe('DiscoverSection', () => { expect(detail.getByRole('status', { name: en.detailLoading })).toBeTruthy() expect(detail.getByRole('heading', { name: 'Alpha' })).toBeTruthy() expect(detail.getByText('dsh-a')).toBeTruthy() - for (const value of ['Jane', 'dsh-a@1.0.0', 'https://r.example/', 'Tools', 'Official', 'Beta', 'Alpha market']) expect(detail.getByText(value)).toBeTruthy() + for (const value of ['Jane', 'dsh-a@1.0.0', 'https://r.example/', 'Tools', 'Official', 'Beta', 'Alpha index']) expect(detail.getByText(value)).toBeTruthy() const homepage = detail.getByRole('link', { name: 'https://example.com/alpha#readme' }) expect([homepage.getAttribute('href'), homepage.getAttribute('target'), homepage.getAttribute('rel')]) .toEqual(['https://example.com/alpha#readme', '_blank', 'noopener noreferrer']) @@ -420,14 +420,14 @@ describe('DiscoverSection', () => { expect(screen.getByRole('status', { name: en.loading })).toBeTruthy() expect(screen.getByRole('button', { name: en.refresh }).hasAttribute('disabled')).toBe(true) set({ listing: { url: B, query: INITIAL_QUERY, status: 'ready', value: { ...LISTING, url: B, entries: [], total: 0 }, answered: INITIAL_QUERY, failure: null } }) - expect(screen.getByRole('button', { name: 'Switch plugin market, currently b.example' })).toBeTruthy() + expect(screen.getByRole('button', { name: 'Switch plugin index, currently b.example' })).toBeTruthy() }) it('manages sources in a dialog with the trust notice and each refusal', () => { const { actions, set } = renderTab({ editing: true, draft: 'https://c.example/index.json', indexes: [{ url: A, title: LISTING.title }, { url: B, error: 'HTTP 404' }] }) const dialog = within(screen.getByRole('dialog', { name: en.sourcesTitle })) expect(dialog.getByText(en.sourcesTrust)).toBeTruthy() - expect(dialog.getByText('Alpha market')).toBeTruthy() + expect(dialog.getByText('Alpha index')).toBeTruthy() expect(dialog.getByText(B)).toBeTruthy() expect(dialog.getByText('HTTP 404')).toBeTruthy() fireEvent.click(dialog.getByRole('button', { name: `Remove ${B}` })) @@ -453,7 +453,7 @@ describe('DiscoverSection', () => { it('renders the Chinese copy and dates', () => { renderTab({}, zh) - expect(screen.getByRole('button', { name: '切换插件市场,当前为 甲市场' })).toBeTruthy() + expect(screen.getByRole('button', { name: '切换插件索引,当前为 甲索引' })).toBeTruthy() expect(card('dsh-a').getByText('甲')).toBeTruthy() expect(card('dsh-a').getByText(/^1\.0\.0 · Jane · 2026年9月/)).toBeTruthy() expect(card('dsh-b').getByText(zh.statusInstalled)).toBeTruthy() diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 7795512361..351d0e9c54 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -18,11 +18,11 @@ en: 10be7f75a1576575 zh: abf2905d89b0da48 /deepseek-ai-dsh-client-ui-primitives/use-this-package/component-catalog: - en: 75e1681dcfc61a14 - zh: 3eda3143730f83e1 + en: 3c79351ff2210ccb + zh: 4cbe07ded89bbaa1 /deepseek-ai-dsh-client-ui-primitives/use-this-package/controls-and-icons: - en: dbd7f0e74c45a762 - zh: 7efbfbf5500cc835 + en: b3744373b4b2c1fb + zh: 2dbde8ee0bfa5f28 /deepseek-ai-dsh-client-ui-primitives/use-this-package/rendering-agent-output: en: 02f310acbae29955 zh: 1e41393d5d714b2d diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 637ff7f55c..e1b9fd40db 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -49,7 +49,7 @@ Check this table before writing a control in a feature package. A plugin cannot | `Input` | Single-line text entry for search boxes and inline forms. Its ref targets the native input for focus and is cleared on unmount. | | `InlineEditor` | Autofocused multi-line inline editor for short drafts such as queued messages and goals. It grows with its text and its width up to six lines, then scrolls; Enter saves, Shift+Enter breaks the line, Enter during IME composition does nothing, and Escape cancels. The caller supplies the localized `label` and rebinds the scrollbar on an elevated surface. | | `ZoomControls`, `DOCUMENT_ZOOM` | Floating document zoom controls with localized labels, fixed presets, and visibility/activity callbacks. An omitted `fit` hides the fitting row. The toolbar carries `data-document-zoom-controls`, plus `data-document-zoom-visible` while visible. An owner `className` overrides `bottom` and `max-width` regardless of stylesheet order; `inset-inline-start` and `transform` center the toolbar and move it on enter and exit, `opacity` fades it, and all three stay with the primitive. Renderers share the published scale limits. | -| `Menu`, `MenuItemButton` | Dropdown of `items` data rows, separators, and group labels, with nested submenus; `children` adds component rows, each a `MenuItemButton` (`separatorBefore` starts a new group), in the same list. Every row shares the styling, the keyboard walk, and the focus return; closing stays the owner's state change for both kinds. While open, ↑/↓ (with Home and End) walk the list, Tab settles the focused row, and Escape or Shift+Tab close back to the anchor; selecting a row also returns the keyboard to the anchor unless the owner moved it itself. Only a keyboard on the anchor or inside the list is intercepted, and `autoFocus` decides solely whether opening focuses the first row. | +| `Menu`, `MenuItemButton` | Dropdown of `items` data rows, separators, and group labels, with nested submenus; `children` adds component rows, each a `MenuItemButton` (`separatorBefore` starts a new group), in the same list. `MenuItemButton.current` exposes the current page or view through `aria-current` without changing selection or appearance. Every row shares the styling, the keyboard walk, and the focus return; closing stays the owner's state change for both kinds. While open, ↑/↓ (with Home and End) walk the list, Tab settles the focused row, and Escape or Shift+Tab close back to the anchor; selecting a row also returns the keyboard to the anchor unless the owner moved it itself. Only a keyboard on the anchor or inside the list is intercepted, and `autoFocus` decides solely whether opening focuses the first row. | | `MenuGroup`, `observeStickyMenuGroups` | Localized, accessible groups for custom menus and listboxes, with shared sticky-heading styling and native intersection/size observation; the caller owns the observer's lifetime. | | `Pill` | Selectable capsule button for view switchers and filters; takes `active` and `onClick`. | | `SegmentedTabs` | Controlled equal-width tabs with a sliding indicator and Left/Right, Home, and End navigation. The caller supplies labels, tab/panel ids, and panel content. Default `size="md"` is 42px tall with a 4px inset; `size="sm"` is 34px tall with a 2px inset. | @@ -96,9 +96,9 @@ Writing your own component in your own package is fine when the need is genuinel ### Controls and icons -The catalog above lists what each export is for; this section covers the behavior that props alone do not show. Product icon names omit artboard sizes and end in `Regular` for the supplied one-pixel artwork or `Medium` for the same geometry at a 1.3px stroke; the `size` prop controls rendered dimensions. Named components keep the chosen weight visible at import sites; the library owns stroke geometry instead of consumer CSS overrides. Each product icon renders an `` box with one `` of its `dsw-icon--` symbol, for example `dsw-icon-search-outline-regular`, and names that id in `data-icon`. `ICON_SYMBOLS` holds every symbol's default SVG document; ui-theme renders the document icon sprite from it, and an icon whose symbol is missing renders nothing. Artwork reached through `` inherits `currentColor` and CSS variables from the use site. Each product, reference, link, and permission glyph intentionally keeps both weight exports even when the current product uses only one, so callers can choose emphasis without adding another API later; fill-only pairs render identically. `IconWarningOutlineRegular`/`Medium` use a circle; `IconWarningTriangleOutlineRegular`/`Medium` use a rounded triangle. `FishLogo` and `BrandWordmark` fill brand slots. `FileTypeIcon` renders the traditional 28px spreadsheet, folder, HTML, image, Markdown, generic, PDF, PPT, video, and Word glyphs, and uses the imported square technology artwork for the established 48 code and configuration categories. The import replaces artwork only: archive-only categories do not extend `CodeFileType`. `classifyFileType` applies exact filename, prefix, suffix, optional project-context, and extension rules in that order; React names win over TypeScript/JavaScript, Angular suffixes win over their base extension, and a Dart file becomes Flutter only when the supplied project files contain a `pubspec.yaml` with `flutter:`. Markdown and SVG remain traditional Markdown and image files. Spreadsheet mappings include CSV, TSV, Excel workbooks and templates, OpenDocument spreadsheets, and Numbers; KEY maps to slides, while RTF/ODT/Pages map to documents. `fileExtension` exposes the same basename and final-dot parsing for adjacent metadata labels. Traditional glyphs use a solid category-colored sheet with a white mark and translucent white corner; the generic code glyph uses angle brackets and a slash at the supplied 1.35px stroke; the generic file uses a grey sheet and darker grey corner. Callers may override the sheet color through `--dsh-file-type-icon-color`. The full-color technology artwork is the deliberate exception and retains its embedded palette. All glyphs are decorative and carry no label. `LinkIconMedium` is the leading glyph for clickable artifact links — globe, folder, code, image, document, or plain paper, and for a `url` link the mark of a well-known site named by its `href` — the developer sites a transcript usually cites (GitHub, GitLab, npm, PyPI, Stack Overflow, MDN, Wikipedia, Hacker News, YouTube, X, Bilibili, Zhihu, Juejin, CSDN) and mainstream search, video, social, shopping, and reference sites (Google, Baidu, DuckDuckGo, TikTok, Netflix, Spotify, Facebook, Instagram, Reddit, Telegram, WhatsApp, WeChat, QQ, Weibo, Taobao, AliExpress, eBay, Quora, V2EX, Apple) — while `classifyLinkPath` folds the shared file types into that existing six-category vocabulary. `ConnectionIndicator` renders a warning-colored disconnected action whose permanent retry glyph marks the retry action beside the owner-supplied outage label, the shared ongoing loader in the same warning color as its label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Clicking either warning state requests an immediate reconnect; no hover interaction changes the copy. The pill fades in on appearance, fades out for 150ms before unmounting, and sizes to its current label. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor; anchored overlays and portalled `Menu` add 20px to the frame's published top clearance, or keep 20px in native fullscreen, while preserving their own minimum margins in ordinary browsers. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. Touch entry does not open its preview or intercept the anchor action. Mouse entry requires an available hover-capable pointer; keyboard focus remains independent of that capability. Its `preview` variant uses the anchor or `widthAnchorRef` element’s width minus 48px, centers the panel with 24px side insets, and places it above or below the row within the viewport. The panel keeps the frame’s top clearance, caps its height at 420px, and follows content and anchor resizing; Escape or pointer and keyboard activation of the anchor dismisses it. The whole preview fades in and out over 100ms; a closing preview stops receiving pointer input and unmounts after the fade. Re-entering the anchor during the fade restores it. Reduced-motion preferences suppress the transition. Copy labels are required only when copying is enabled. `HoverCard` and nested action tooltips display independently; use `Tooltip.portal` to layer the small bubble above the card. A visible nested `Tooltip` suppresses only its enclosing tooltip, not the hover card. `Toast` uses the owner's `holdMs` for both the fade delay and its hold-and-fade lifetime. Rerenders with unchanged `holdMs` do not restart that lifetime; completion uses the latest callback, and fully faded actions cannot receive input. A new component key restarts the banner. `rankByName` is the `/` menu's shared candidate ranker for the command and skill sources: the query must be a case-insensitive ordered subsequence of the name; prefix hits rank first, then alignment score, then source order. Portalled `Menu` lists follow their anchor during dragging and CSS transforms, and stop tracking when closed. `Menu.autoFocus` focuses its first enabled item, supports Arrow Up/Down and Home/End navigation, and focuses the first button in the anchor on Escape; it is opt-in for action menus. +The catalog above lists what each export is for; this section covers the behavior that props alone do not show. Product icon names omit artboard sizes and end in `Regular` for the supplied one-pixel artwork or `Medium` for the same geometry at a 1.3px stroke; the `size` prop controls rendered dimensions. Named components keep the chosen weight visible at import sites; the library owns stroke geometry instead of consumer CSS overrides. Each product icon renders an `` box with one `` of its `dsw-icon--` symbol, for example `dsw-icon-search-outline-regular`, and names that id in `data-icon`. `ICON_SYMBOLS` holds every symbol's default SVG document; ui-theme renders the document icon sprite from it, and an icon whose symbol is missing renders nothing. Artwork reached through `` inherits `currentColor` and CSS variables from the use site. Each product, reference, link, and permission glyph intentionally keeps both weight exports even when the current product uses only one, so callers can choose emphasis without adding another API later; fill-only pairs render identically. `IconWarningOutlineRegular`/`Medium` use a circle; `IconWarningTriangleOutlineRegular`/`Medium` use a rounded triangle. `FishLogo` and `BrandWordmark` fill brand slots. `FileTypeIcon` renders the traditional 28px spreadsheet, folder, HTML, image, Markdown, generic, PDF, PPT, video, and Word glyphs, and uses the imported square technology artwork for the established 48 code and configuration categories. The import replaces artwork only: archive-only categories do not extend `CodeFileType`. `classifyFileType` applies exact filename, prefix, suffix, optional project-context, and extension rules in that order; React names win over TypeScript/JavaScript, Angular suffixes win over their base extension, and a Dart file becomes Flutter only when the supplied project files contain a `pubspec.yaml` with `flutter:`. Markdown and SVG remain traditional Markdown and image files. Spreadsheet mappings include CSV, TSV, Excel workbooks and templates, OpenDocument spreadsheets, and Numbers; KEY maps to slides, while RTF/ODT/Pages map to documents. `fileExtension` exposes the same basename and final-dot parsing for adjacent metadata labels. Traditional glyphs use a solid category-colored sheet with a white mark and translucent white corner; the generic code glyph uses angle brackets and a slash at the supplied 1.35px stroke; the generic file uses a grey sheet and darker grey corner. Callers may override the sheet color through `--dsh-file-type-icon-color`. The full-color technology artwork is the deliberate exception and retains its embedded palette. All glyphs are decorative and carry no label. `LinkIconMedium` is the leading glyph for clickable artifact links — globe, folder, code, image, document, or plain paper, and for a `url` link the mark of a well-known site named by its `href` — the developer sites a transcript usually cites (GitHub, GitLab, npm, PyPI, Stack Overflow, MDN, Wikipedia, Hacker News, YouTube, X, Bilibili, Zhihu, Juejin, CSDN) and mainstream search, video, social, shopping, and reference sites (Google, Baidu, DuckDuckGo, TikTok, Netflix, Spotify, Facebook, Instagram, Reddit, Telegram, WhatsApp, WeChat, QQ, Weibo, Taobao, AliExpress, eBay, Quora, V2EX, Apple) — while `classifyLinkPath` folds the shared file types into that existing six-category vocabulary. `ConnectionIndicator` renders a warning-colored disconnected action whose permanent retry glyph marks the retry action beside the owner-supplied outage label, the shared ongoing loader in the same warning color as its label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Clicking either warning state requests an immediate reconnect; no hover interaction changes the copy. The pill fades in on appearance, fades out for 150ms before unmounting, and sizes to its current label. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor; anchored overlays and portalled `Menu` add 20px to the frame's published top clearance, or keep 20px in native fullscreen, while preserving their own minimum margins in ordinary browsers; while an anchor is inside that top strip, its overlay, including a `HoverCard`, is never pushed lower than the anchor's bottom edge plus the overlay's gap. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. Touch entry does not open its preview or intercept the anchor action. Mouse entry requires an available hover-capable pointer; keyboard focus remains independent of that capability. Its `preview` variant uses the anchor or `widthAnchorRef` element’s width minus 48px, centers the panel with 24px side insets, and places it above or below the row within the viewport. The panel keeps the frame’s top clearance except below an anchor inside the top strip, caps its height at 420px, and follows content and anchor resizing; Escape or pointer and keyboard activation of the anchor dismisses it. The whole preview fades in and out over 100ms; a closing preview stops receiving pointer input and unmounts after the fade. Re-entering the anchor during the fade restores it. Reduced-motion preferences suppress the transition. Copy labels are required only when copying is enabled. `HoverCard` and nested action tooltips display independently; use `Tooltip.portal` to layer the small bubble above the card. A visible nested `Tooltip` suppresses only its enclosing tooltip, not the hover card. `Toast` uses the owner's `holdMs` for both the fade delay and its hold-and-fade lifetime. Rerenders with unchanged `holdMs` do not restart that lifetime; completion uses the latest callback, and fully faded actions cannot receive input. A new component key restarts the banner. `rankByName` is the `/` menu's shared candidate ranker for the command and skill sources: the query must be a case-insensitive ordered subsequence of the name; prefix hits rank first, then alignment score, then source order. Portalled `Menu` lists follow their anchor during dragging and CSS transforms, and stop tracking when closed. `Menu.autoFocus` focuses its first enabled item, supports Arrow Up/Down and Home/End navigation, and focuses the first button in the anchor on Escape; it is opt-in for action menus. -`Tooltip` reads the anchor on hover or keyboard focus and fits its bubble from `ResizeObserver` border-box sizes. It stays hidden until its first fit, prefers the requested side, then tries the opposite and perpendicular sides, sliding along the anchor without covering it when a side fits. Horizontal placements keep a 10px gap; vertical placements use `gap` (8px by default). Width and height stay within a 12px viewport margin; text wraps, and content taller than the viewport scrolls when pinned. If no side fits, a stable viewport-bounded fallback can overlap the anchor. Viewport resize refreshes the anchor coordinates; label-size changes reuse them. Fitting does not synchronously measure the bubble or trigger a React render. Touch presses dismiss unpinned hints and do not trigger hover hints, including compatibility mouse events. Mouse entry opens hints only while `(any-hover: hover)` reports an available hover-capable input, excluding iOS mouse entry caused by a tapped icon changing. Keyboard focus and explicit click-to-open explanations remain available on touch-only devices. Opt-in `openOnClick` lets an informational button pin the bubble by click or tap and associates it with the anchor description. Another click or tap, Escape, Tab, or an outside pointerdown closes it. Ordinary action buttons execute their action directly without a touch tooltip. +`Tooltip` reads the anchor on hover or keyboard focus and fits its bubble from `ResizeObserver` border-box sizes. It stays hidden until its first fit, prefers the requested side, then tries the opposite and perpendicular sides, sliding along the anchor without covering it when a side fits. Horizontal placements keep a 10px gap; vertical placements use `gap` (8px by default). Width and height stay within a 12px viewport margin; text wraps, and content taller than the viewport scrolls when pinned. If no side fits, a stable viewport-bounded fallback can overlap the anchor. Viewport resize refreshes the anchor coordinates; label-size changes reuse them. Fitting does not synchronously measure the bubble or trigger a React render. Touch presses dismiss unpinned hints and do not trigger hover hints, including compatibility mouse events. Mouse entry opens hints only while `(any-hover: hover)` reports an available hover-capable input, excluding iOS mouse entry caused by a tapped icon changing. Keyboard focus and explicit click-to-open explanations remain available on touch-only devices. Opt-in `openOnClick` lets an informational button pin the bubble by click or tap and associates it with the anchor description. Another click or tap, Escape, Tab, or an outside pointerdown closes it. Ordinary action buttons execute their action directly without a touch tooltip. Opt-in `onlyWhenClipped` shows the bubble only when the anchor or an HTML element inside it overflows horizontally when the bubble is about to show, so a label that repeats ellipsis-cut text appears only while it adds information. diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index e10743c8f9..bf2b60c2af 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -49,7 +49,7 @@ kind: "package-library" | `Input` | 单行文本输入,用于搜索框与行内表单。ref 指向原生输入框,供焦点控制使用,并在卸载时清空。 | | `InlineEditor` | 自动聚焦的多行行内编辑器,用于排队消息、goal 等短草稿。它随文本和宽度增高,最多六行,之后滚动;Enter 保存,Shift+Enter 换行,输入法组合中的 Enter 不生效,Escape 取消。调用方提供本地化 `label`,并在 elevated 表面上重绑滚动条。 | | `ZoomControls`, `DOCUMENT_ZOOM` | 悬浮文档缩放控件,接收本地化标签、固定预设,以及可见性和交互状态回调。省略 `fit` 时不显示适配项。工具栏带 `data-document-zoom-controls`,可见时另带 `data-document-zoom-visible`。owner 的 `className` 覆盖 `bottom` 与 `max-width`,不受样式表加载顺序影响;`inset-inline-start` 和 `transform` 负责居中与进出场位移,`opacity` 负责淡入淡出,三者都由原语保留。渲染器共用导出的缩放范围。 | -| `Menu`, `MenuItemButton` | 由 `items` 数据行、分隔线与分组标题构成的下拉菜单,支持嵌套子菜单;`children` 在同一列表中加入组件行,每行一个 `MenuItemButton`(`separatorBefore` 开启新分组)。所有行共享样式、键盘走位与焦点归还;两类行的关闭都是 owner 状态的改变。打开期间 `↑`/`↓`(以及 Home、End)在列表中走位,Tab 选定聚焦行,Escape 或 Shift+Tab 关闭并把焦点还给锚点;选定一行同样把键盘还给锚点——除非拥有者自己移动了焦点。只拦截位于锚点或列表内的键盘,`autoFocus` 仅决定打开时是否聚焦首行。 | +| `Menu`, `MenuItemButton` | 由 `items` 数据行、分隔线与分组标题构成的下拉菜单,支持嵌套子菜单;`children` 在同一列表中加入组件行,每行一个 `MenuItemButton`(`separatorBefore` 开启新分组)。`MenuItemButton.current` 通过 `aria-current` 标识当前页面或视图,不改变选中状态或外观。所有行共享样式、键盘走位与焦点归还;两类行的关闭都是 owner 状态的改变。打开期间 `↑`/`↓`(以及 Home、End)在列表中走位,Tab 选定聚焦行,Escape 或 Shift+Tab 关闭并把焦点还给锚点;选定一行同样把键盘还给锚点——除非拥有者自己移动了焦点。只拦截位于锚点或列表内的键盘,`autoFocus` 仅决定打开时是否聚焦首行。 | | `MenuGroup`、`observeStickyMenuGroups` | 为自定义菜单与列表框提供本地化、可访问的分组,共用吸顶标题样式与原生交叉/尺寸观察;调用方负责观察器的生命周期。 | | `Pill` | 可选中的胶囊按钮,用于视图切换与筛选器;接受 `active` 与 `onClick`。 | | `SegmentedTabs` | 受控的等宽分段标签,支持滑动指示条及左/右方向键、Home、End 导航。调用方提供文案、标签与面板 id,以及面板内容。默认 `size="md"` 总高 42px、内边距 4px;`size="sm"` 总高 34px、内边距 2px。 | @@ -96,9 +96,9 @@ kind: "package-library" ### 控件与图标 -上面的目录说明每个导出的用途;本节讲 props 本身看不出来的行为。产品图标名称不含画板尺寸,以 `Regular` 表示原始 1px 图形,以 `Medium` 表示同一几何的 1.3px 描边;`size` prop 控制渲染尺寸。具名组件让所选线重在导入处可见;描边几何由组件库拥有,而不是由消费方 CSS 覆盖。每个产品图标渲染一个 `` 框,其中包含一个引用对应 `dsw-icon--` symbol 的 ``,例如 `dsw-icon-search-outline-regular`,并在 `data-icon` 中写出该 id。`ICON_SYMBOLS` 保存每个 symbol 的默认 SVG 文档;ui-theme 据此渲染文档的图标 sprite,缺少对应 symbol 的图标不渲染任何内容。通过 `` 引用的图形会从使用处继承 `currentColor` 与 CSS 变量。每个产品、引用、链接与权限图形都会有意保留两种线重导出,即使当前产品只使用其中一种,也让调用方无需再次扩展 API 就能选择强调程度;仅填充的成对图形外观相同。`IconWarningOutlineRegular`/`Medium` 使用圆形;`IconWarningTriangleOutlineRegular`/`Medium` 使用圆角三角形。`FishLogo` 与 `BrandWordmark` 填充品牌 slot。`FileTypeIcon` 渲染传统的 28px spreadsheet、folder、HTML、image、Markdown、generic、PDF、PPT、video 与 Word 图形,并为现有 48 个代码和配置类别使用导入的方形技术图形。该导入只替换图形:资源包中额外的类别不会扩展 `CodeFileType`。`classifyFileType` 按完整文件名、前缀、后缀、可选项目上下文、扩展名的顺序匹配;React 文件名优先于 TypeScript/JavaScript,Angular 后缀优先于基础扩展名,只有传入的项目文件包含带 `flutter:` 的 `pubspec.yaml` 时 Dart 文件才使用 Flutter。Markdown 与 SVG 仍分别使用传统 Markdown 与图片图形。表格映射包括 CSV、TSV、Excel 工作簿与模板、OpenDocument 表格和 Numbers;KEY 映射为幻灯片,RTF/ODT/Pages 映射为文档。`fileExtension` 为相邻元数据 label 暴露同一套 basename 与最终点号解析。传统图形使用实色分类底板、白色标记和半透明白色折角;通用代码图形使用尖括号与斜线,保留原图 1.35px 描边;通用文件使用灰色底板与较深灰色折角。调用方可通过 `--dsh-file-type-icon-color` 覆盖底板颜色。全彩技术图形是明确例外,会保留其内嵌调色板。所有图形都是装饰性的,不自带 label。`LinkIconMedium` 是可点击产物链接的前置图形——地球、文件夹、代码、图片、文档或纸张,`url` 链接的 `href` 指向已知站点时则改用该站点自己的标记——转写内容常引用的开发者站点(GitHub、GitLab、npm、PyPI、Stack Overflow、MDN、Wikipedia、Hacker News、YouTube、X、Bilibili、知乎、掘金、CSDN),以及主流搜索、视频、社交、购物与参考资料站点(Google、百度、DuckDuckGo、TikTok、Netflix、Spotify、Facebook、Instagram、Reddit、Telegram、WhatsApp、微信、QQ、微博、淘宝、速卖通、eBay、Quora、V2EX、Apple)——`classifyLinkPath` 把共享文件类型折叠进原有六类词汇。`ConnectionIndicator` 可渲染警告色的断联操作(常驻重试图形指明重试动作,断联文案由持有方提供)、与文案使用相同警告色的共享 ongoing loading 加一至三个点以独立于 retry 时序的 500ms 节奏推进的连接中状态,或成功色的恢复状态。点击任一警告状态都会请求立即重连;没有任何悬停交互会改变文案。药丸出现时淡入、卸载前淡出 150ms,宽度随当前 label 自适应。它的持有方提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点;锚定浮层与 portal 模式的 `Menu` 在框架发布的顶部占用量上增加 20px,原生全屏时保留 20px,普通浏览器中沿用自身的最小边距。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。触摸进入不会打开预览,也不会拦截锚点操作。鼠标进入要求存在可悬停的指针设备;键盘聚焦不依赖该能力。其 `preview` 变体使用 anchor 或 `widthAnchorRef` 元素的宽度减去 48px,左右各内缩 24px,并在视口内放置于行的上方或下方。浮层避开框架顶部保留区,高度最多 420px,会跟随内容及 anchor 尺寸变化;Escape 或通过鼠标和键盘激活锚点可将其关闭。整个浮层的淡入和淡出各持续 100ms;关闭中的浮层停止接收指针输入,淡出后卸载。在淡出期间移回锚点可恢复显示。减少动态效果偏好会禁用过渡动画。只有启用复制时才必须提供复制标签。`HoverCard` 与嵌套的操作提示独立显示;使用 `Tooltip.portal` 将小气泡置于卡片上层。可见的嵌套 `Tooltip` 只隐藏外层 tooltip,不隐藏悬停卡片。`Toast` 使用调用方的 `holdMs` 同时控制淡出延迟与停留加淡出的总时长。`holdMs` 未变时,父组件重渲染不会重启该生命周期;完成时调用最新回调,完全淡出的操作不能接收输入。新的组件 key 会重新开始横幅周期。`rankByName` 是 `/` 菜单命令源与 skill(技能)源共享的候选排序器:查询必须是名字的不区分大小写的有序子序列;前缀命中排最前,其次按对齐分数,再按来源顺序。 portal 模式的 `Menu` 列表会在拖动和 CSS 变换期间跟随锚点,并在关闭时停止跟踪。`Menu.autoFocus` 聚焦首个启用项,支持上下方向键与 Home/End 导航,并在 Escape 时聚焦 anchor 内的第一个按钮;操作菜单可显式启用。 +上面的目录说明每个导出的用途;本节讲 props 本身看不出来的行为。产品图标名称不含画板尺寸,以 `Regular` 表示原始 1px 图形,以 `Medium` 表示同一几何的 1.3px 描边;`size` prop 控制渲染尺寸。具名组件让所选线重在导入处可见;描边几何由组件库拥有,而不是由消费方 CSS 覆盖。每个产品图标渲染一个 `` 框,其中包含一个引用对应 `dsw-icon--` symbol 的 ``,例如 `dsw-icon-search-outline-regular`,并在 `data-icon` 中写出该 id。`ICON_SYMBOLS` 保存每个 symbol 的默认 SVG 文档;ui-theme 据此渲染文档的图标 sprite,缺少对应 symbol 的图标不渲染任何内容。通过 `` 引用的图形会从使用处继承 `currentColor` 与 CSS 变量。每个产品、引用、链接与权限图形都会有意保留两种线重导出,即使当前产品只使用其中一种,也让调用方无需再次扩展 API 就能选择强调程度;仅填充的成对图形外观相同。`IconWarningOutlineRegular`/`Medium` 使用圆形;`IconWarningTriangleOutlineRegular`/`Medium` 使用圆角三角形。`FishLogo` 与 `BrandWordmark` 填充品牌 slot。`FileTypeIcon` 渲染传统的 28px spreadsheet、folder、HTML、image、Markdown、generic、PDF、PPT、video 与 Word 图形,并为现有 48 个代码和配置类别使用导入的方形技术图形。该导入只替换图形:资源包中额外的类别不会扩展 `CodeFileType`。`classifyFileType` 按完整文件名、前缀、后缀、可选项目上下文、扩展名的顺序匹配;React 文件名优先于 TypeScript/JavaScript,Angular 后缀优先于基础扩展名,只有传入的项目文件包含带 `flutter:` 的 `pubspec.yaml` 时 Dart 文件才使用 Flutter。Markdown 与 SVG 仍分别使用传统 Markdown 与图片图形。表格映射包括 CSV、TSV、Excel 工作簿与模板、OpenDocument 表格和 Numbers;KEY 映射为幻灯片,RTF/ODT/Pages 映射为文档。`fileExtension` 为相邻元数据 label 暴露同一套 basename 与最终点号解析。传统图形使用实色分类底板、白色标记和半透明白色折角;通用代码图形使用尖括号与斜线,保留原图 1.35px 描边;通用文件使用灰色底板与较深灰色折角。调用方可通过 `--dsh-file-type-icon-color` 覆盖底板颜色。全彩技术图形是明确例外,会保留其内嵌调色板。所有图形都是装饰性的,不自带 label。`LinkIconMedium` 是可点击产物链接的前置图形——地球、文件夹、代码、图片、文档或纸张,`url` 链接的 `href` 指向已知站点时则改用该站点自己的标记——转写内容常引用的开发者站点(GitHub、GitLab、npm、PyPI、Stack Overflow、MDN、Wikipedia、Hacker News、YouTube、X、Bilibili、知乎、掘金、CSDN),以及主流搜索、视频、社交、购物与参考资料站点(Google、百度、DuckDuckGo、TikTok、Netflix、Spotify、Facebook、Instagram、Reddit、Telegram、WhatsApp、微信、QQ、微博、淘宝、速卖通、eBay、Quora、V2EX、Apple)——`classifyLinkPath` 把共享文件类型折叠进原有六类词汇。`ConnectionIndicator` 可渲染警告色的断联操作(常驻重试图形指明重试动作,断联文案由持有方提供)、与文案使用相同警告色的共享 ongoing loading 加一至三个点以独立于 retry 时序的 500ms 节奏推进的连接中状态,或成功色的恢复状态。点击任一警告状态都会请求立即重连;没有任何悬停交互会改变文案。药丸出现时淡入、卸载前淡出 150ms,宽度随当前 label 自适应。它的持有方提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点;锚定浮层与 portal 模式的 `Menu` 在框架发布的顶部占用量上增加 20px,原生全屏时保留 20px,普通浏览器中沿用自身的最小边距;锚点位于该顶部区域内时,其浮层(包括 `HoverCard`)不会被推到低于锚点底边加浮层间距的位置。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。触摸进入不会打开预览,也不会拦截锚点操作。鼠标进入要求存在可悬停的指针设备;键盘聚焦不依赖该能力。其 `preview` 变体使用 anchor 或 `widthAnchorRef` 元素的宽度减去 48px,左右各内缩 24px,并在视口内放置于行的上方或下方。浮层避开框架顶部保留区(位于顶部区域内的锚点下方时除外),高度最多 420px,会跟随内容及 anchor 尺寸变化;Escape 或通过鼠标和键盘激活锚点可将其关闭。整个浮层的淡入和淡出各持续 100ms;关闭中的浮层停止接收指针输入,淡出后卸载。在淡出期间移回锚点可恢复显示。减少动态效果偏好会禁用过渡动画。只有启用复制时才必须提供复制标签。`HoverCard` 与嵌套的操作提示独立显示;使用 `Tooltip.portal` 将小气泡置于卡片上层。可见的嵌套 `Tooltip` 只隐藏外层 tooltip,不隐藏悬停卡片。`Toast` 使用调用方的 `holdMs` 同时控制淡出延迟与停留加淡出的总时长。`holdMs` 未变时,父组件重渲染不会重启该生命周期;完成时调用最新回调,完全淡出的操作不能接收输入。新的组件 key 会重新开始横幅周期。`rankByName` 是 `/` 菜单命令源与 skill(技能)源共享的候选排序器:查询必须是名字的不区分大小写的有序子序列;前缀命中排最前,其次按对齐分数,再按来源顺序。 portal 模式的 `Menu` 列表会在拖动和 CSS 变换期间跟随锚点,并在关闭时停止跟踪。`Menu.autoFocus` 聚焦首个启用项,支持上下方向键与 Home/End 导航,并在 Escape 时聚焦 anchor 内的第一个按钮;操作菜单可显式启用。 -`Tooltip` 在悬停或键盘聚焦时读取锚点位置,再根据 `ResizeObserver` 提供的边框盒尺寸调整气泡。首次定位前保持隐藏,优先使用指定方向,再尝试相反方向和垂直于该方向的两侧;只要某一侧容得下,就沿锚点平移而不遮挡锚点。左右放置保留 10px 间距,上下放置使用 `gap`(默认 8px)。宽高限制在视口内并保留 12px 边距;文本自动换行,超出视口高度的内容在固定气泡后可滚动。若所有方向都放不下,则使用稳定的视口内回退位置,可能遮挡锚点。视口尺寸变化会重新读取锚点坐标,标签尺寸变化则复用坐标;定位不会同步测量气泡,也不会触发 React 渲染。 触摸按下会关闭未固定的提示,且不会触发悬停提示,包括触摸后的兼容鼠标事件。只有 `(any-hover: hover)` 表明存在可悬停的输入设备时,鼠标进入才会打开提示,从而排除 iOS 因已点击图标变化而生成的鼠标进入事件。仅有触摸输入的设备仍可使用键盘聚焦提示和显式点击打开的说明。信息按钮可显式启用 `openOnClick`,通过鼠标或触摸点击固定气泡供阅读,并将内容关联为锚点的无障碍描述;再次点击、Escape、Tab 或外部 pointerdown 会关闭。普通操作按钮在触摸点击时直接执行操作,不显示 Tooltip。 +`Tooltip` 在悬停或键盘聚焦时读取锚点位置,再根据 `ResizeObserver` 提供的边框盒尺寸调整气泡。首次定位前保持隐藏,优先使用指定方向,再尝试相反方向和垂直于该方向的两侧;只要某一侧容得下,就沿锚点平移而不遮挡锚点。左右放置保留 10px 间距,上下放置使用 `gap`(默认 8px)。宽高限制在视口内并保留 12px 边距;文本自动换行,超出视口高度的内容在固定气泡后可滚动。若所有方向都放不下,则使用稳定的视口内回退位置,可能遮挡锚点。视口尺寸变化会重新读取锚点坐标,标签尺寸变化则复用坐标;定位不会同步测量气泡,也不会触发 React 渲染。 触摸按下会关闭未固定的提示,且不会触发悬停提示,包括触摸后的兼容鼠标事件。只有 `(any-hover: hover)` 表明存在可悬停的输入设备时,鼠标进入才会打开提示,从而排除 iOS 因已点击图标变化而生成的鼠标进入事件。仅有触摸输入的设备仍可使用键盘聚焦提示和显式点击打开的说明。信息按钮可显式启用 `openOnClick`,通过鼠标或触摸点击固定气泡供阅读,并将内容关联为锚点的无障碍描述;再次点击、Escape、Tab 或外部 pointerdown 会关闭。普通操作按钮在触摸点击时直接执行操作,不显示 Tooltip。可选的 `onlyWhenClipped` 只在气泡即将显示时锚点或其内部某个 HTML 元素横向溢出才显示气泡,因此重复被省略号截断文本的标签只在能补充信息时出现。 diff --git a/packages/client/ui-primitives/src/HoverCard.tsx b/packages/client/ui-primitives/src/HoverCard.tsx index 157cf35f06..f11c6bad30 100644 --- a/packages/client/ui-primitives/src/HoverCard.tsx +++ b/packages/client/ui-primitives/src/HoverCard.tsx @@ -33,7 +33,8 @@ interface CardPosition { * readable and selectable, but it carries no dismissal affordance of its own. * @param props.openDelayMs - hover dwell before the card shows (default 500). * @param props.variant - compact card beside the anchor, or a preview above/below it - * with 24px side insets, a 420px height cap, frame-top clearance, and 100ms opacity transitions. + * with 24px side insets, a 420px height cap, the frame-top clearance (capped below an anchor inside the top strip), + * and 100ms opacity transitions. * @param props.widthAnchorRef - optional element whose width and horizontal position size the preview. * @param props.inline - keep the anchor in prose; show a contained preview on hover or keyboard focus. * Its hover region connects the anchor and preview across the visual gap. @@ -178,7 +179,7 @@ export function HoverCard({ if (variant === 'preview') { const bounds = widthAnchorRef?.current?.getBoundingClientRect() ?? r const width = Math.max(0, Math.min(bounds.width - PREVIEW_INSET * 2, window.innerWidth - VIEWPORT_MARGIN * 2)) - const topMargin = overlayTopMargin(VIEWPORT_MARGIN) + const topMargin = overlayTopMargin(VIEWPORT_MARGIN, r, ANCHOR_GAP) const belowTop = Math.max(topMargin, r.bottom + ANCHOR_GAP) const above = Math.max(0, r.top - ANCHOR_GAP - topMargin) const below = Math.max(0, window.innerHeight - belowTop - VIEWPORT_MARGIN) @@ -195,7 +196,7 @@ export function HoverCard({ const height = Math.max(h, cardRef.current?.scrollHeight ?? 0) const maxWidth = Math.max(0, Math.min(INLINE_PREVIEW_WIDTH, window.innerWidth - VIEWPORT_MARGIN * 2)) const width = fitContent ? Math.min(cardRef.current?.offsetWidth ?? maxWidth, maxWidth) : maxWidth - const topMargin = overlayTopMargin(VIEWPORT_MARGIN) + const topMargin = overlayTopMargin(VIEWPORT_MARGIN, r, ANCHOR_GAP) const belowTop = Math.max(topMargin, r.bottom + ANCHOR_GAP) const above = Math.max(0, r.top - ANCHOR_GAP - topMargin) const below = Math.max(0, window.innerHeight - belowTop - VIEWPORT_MARGIN) diff --git a/packages/client/ui-primitives/src/Menu.tsx b/packages/client/ui-primitives/src/Menu.tsx index 0f1225fc42..68b3a5766e 100644 --- a/packages/client/ui-primitives/src/Menu.tsx +++ b/packages/client/ui-primitives/src/Menu.tsx @@ -51,6 +51,8 @@ export interface MenuItemButtonProps { shortcut?: MenuItem['shortcut'] /** Leading icon (figma .Menu_cell gap 8). */ icon?: ReactNode + /** Whether this row identifies the currently displayed page or view. */ + current?: boolean /** Whether the row cannot be activated. */ disabled?: boolean /** Destructive row: error-colored text/icon and danger hover fill. */ @@ -75,6 +77,7 @@ export interface MenuItemButtonProps { * @param props.children - visible row label. * @param props.shortcut - effective key labels and accessible combination. * @param props.icon - optional leading icon. + * @param props.current - whether the row identifies the currently displayed page or view. * @param props.disabled - whether the row cannot be activated. * @param props.danger - whether to use the destructive row colors. * @param props.separatorBefore - whether this row starts a new group (hairline above it). @@ -82,7 +85,7 @@ export interface MenuItemButtonProps { * @returns one menu-item row. */ export function MenuItemButton({ - children, shortcut, icon, disabled = false, danger = false, separatorBefore = false, onSelect, + children, shortcut, icon, current, disabled = false, danger = false, separatorBefore = false, onSelect, }: MenuItemButtonProps) { return (
@@ -93,6 +96,7 @@ export function MenuItemButton({ className={clsx(css.item, danger && css.danger)} disabled={disabled} aria-keyshortcuts={shortcut?.aria} + aria-current={current === true ? 'page' : undefined} onClick={onSelect} > {icon !== undefined && {icon}} @@ -255,6 +259,7 @@ export function Menu({ open, anchor, items = [], children, selectedId, selectedI } if (r === null) return const MARGIN = 12 + const GAP = 4 const vw = window.innerWidth const vh = window.innerHeight const listEl = listRef.current @@ -268,18 +273,18 @@ export function Menu({ open, anchor, items = [], children, selectedId, selectedI let x: number let y: number if (side === 'right') { - x = r.right + 4 + x = r.right + GAP y = r.top } else if (align === 'start') { x = r.left - y = side === 'bottom' ? r.bottom + 4 : r.top - lh - 4 + y = side === 'bottom' ? r.bottom + GAP : r.top - lh - GAP } else { x = r.right - lw - y = side === 'bottom' ? r.bottom + 4 : r.top - lh - 4 + y = side === 'bottom' ? r.bottom + GAP : r.top - lh - GAP } if (lw > 0) x = Math.min(Math.max(x, MARGIN), vw - lw - MARGIN) - if (lh > 0) y = Math.min(Math.max(y, overlayTopMargin(MARGIN)), vh - lh - MARGIN) + if (lh > 0) y = Math.min(Math.max(y, overlayTopMargin(MARGIN, r, GAP)), vh - lh - MARGIN) setFixedPos(current => current?.left === x && current.top === y ? current : { left: x, top: y }) } diff --git a/packages/client/ui-primitives/src/Tooltip.tsx b/packages/client/ui-primitives/src/Tooltip.tsx index 37ee6aa831..573afa4515 100644 --- a/packages/client/ui-primitives/src/Tooltip.tsx +++ b/packages/client/ui-primitives/src/Tooltip.tsx @@ -33,6 +33,11 @@ interface AnchorProps { type TooltipLabel = string | (() => string) +function clipped(anchor: HTMLElement): boolean { + return [anchor, ...anchor.querySelectorAll('*')] + .some(el => el instanceof HTMLElement && el.scrollWidth > el.clientWidth) +} + /** * Attach a hover/focus tooltip to an anchor element. * @param props.label - bubble text, or a resolver evaluated only while visible; an empty string shows only shortcut keys. @@ -54,6 +59,9 @@ type TooltipLabel = string | (() => string) * half-viewport cap would render a slab wider than the surface the anchor sits on; always bounded by viewport margins. * @param props.openOnClick - clicking or tapping also pins the bubble for reading; another click, Escape, * Tab, or an outside pointerdown dismisses it. Defaults to false for ordinary action tooltips. + * @param props.onlyWhenClipped - show the bubble only when the anchor or an HTML element inside it + * overflows horizontally, measured when the bubble is about to show; for labels that repeat + * text an ellipsis may cut off. Defaults to false. * @param props.children - a single anchor element; its own ref (callback or object) is forwarded alongside the tooltip's. * @returns the cloned anchor plus a fixed-position bubble, optionally portaled to the body. * The bubble stays hidden until ResizeObserver supplies its size for viewport fitting; clicking the @@ -62,7 +70,7 @@ type TooltipLabel = string | (() => string) * tooltips; tapping still invokes the child's action and can open an openOnClick explanation. * When no side fits, the viewport-bounded fallback may overlap the anchor; oversized content is scrollable when pinned. */ -export function Tooltip({ label, shortcutKeys, side = 'right', align = 'center', delayMs = 0, focusDelayMs = 0, gap = 8, disabled = false, portal = false, maxWidth, openOnClick = false, children }: { label: TooltipLabel; shortcutKeys?: readonly string[] | undefined; side?: TooltipSide; align?: 'center' | 'end'; delayMs?: number; focusDelayMs?: number; gap?: number; disabled?: boolean; portal?: boolean; maxWidth?: number; openOnClick?: boolean; children: ReactElement }) { +export function Tooltip({ label, shortcutKeys, side = 'right', align = 'center', delayMs = 0, focusDelayMs = 0, gap = 8, disabled = false, portal = false, maxWidth, openOnClick = false, onlyWhenClipped = false, children }: { label: TooltipLabel; shortcutKeys?: readonly string[] | undefined; side?: TooltipSide; align?: 'center' | 'end'; delayMs?: number; focusDelayMs?: number; gap?: number; disabled?: boolean; portal?: boolean; maxWidth?: number; openOnClick?: boolean; onlyWhenClipped?: boolean; children: ReactElement }) { const id = useId() const [pinned, setPinned] = useState(false) const anchor = useRef(null) @@ -156,6 +164,7 @@ export function Tooltip({ label, shortcutKeys, side = 'right', align = 'center', const el = anchor.current /* v8 ignore next -- the ref is attached by event time: events fire on the cloned anchor. */ if (el === null) return + if (onlyWhenClipped && !clipped(el)) return setPos(el.getBoundingClientRect()) announce(true) } diff --git a/packages/client/ui-primitives/src/overlay-top-margin.ts b/packages/client/ui-primitives/src/overlay-top-margin.ts index 02ed679280..bb12ff729c 100644 --- a/packages/client/ui-primitives/src/overlay-top-margin.ts +++ b/packages/client/ui-primitives/src/overlay-top-margin.ts @@ -3,11 +3,16 @@ /** * Resolve the top margin an overlay keeps from the viewport edge. * @param min - the overlay's own viewport margin in px, used as the floor. - * @returns the larger of `min` and the frame clearance plus 20px; fullscreen keeps only the 20px gap. + * @param anchor - the overlay's anchor rect in viewport coordinates. While the anchor's top edge is inside the + * viewport, the margin never exceeds `anchor.bottom + gap`, so an overlay opened from a control inside the top strip + * stays next to it. + * @param gap - the distance the overlay keeps below `anchor`. + * @returns the frame clearance plus 20px (20px in fullscreen), capped by a visible `anchor`, but never below `min`. */ -export function overlayTopMargin(min: number): number { +export function overlayTopMargin(min: number, anchor?: { readonly top: number; readonly bottom: number }, gap = 0): number { const root = document.documentElement const clearance = Number.parseFloat(getComputedStyle(root).getPropertyValue('--dsh-frame-top-clearance')) if (Number.isNaN(clearance)) return min - return Math.max(min, (root.hasAttribute('data-fullscreen') ? 0 : clearance) + 20) + const cap = anchor !== undefined && anchor.top >= 0 ? anchor.bottom + gap : Number.POSITIVE_INFINITY + return Math.max(min, Math.min((root.hasAttribute('data-fullscreen') ? 0 : clearance) + 20, cap)) } diff --git a/packages/client/ui-primitives/src/useAnchoredPosition.ts b/packages/client/ui-primitives/src/useAnchoredPosition.ts index f87e4d0697..11256ae00a 100644 --- a/packages/client/ui-primitives/src/useAnchoredPosition.ts +++ b/packages/client/ui-primitives/src/useAnchoredPosition.ts @@ -56,7 +56,7 @@ export function useAnchoredPosition(options: AnchoredPositionOptions): CSSProper let left = align === 'end' ? rect.right - width : rect.left let top = side === 'top' ? rect.top - gap - height : rect.bottom + gap if (width > 0) left = Math.min(Math.max(left, margin), window.innerWidth - width - margin) - if (height > 0) top = Math.min(Math.max(top, overlayTopMargin(margin)), window.innerHeight - height - margin) + if (height > 0) top = Math.min(Math.max(top, overlayTopMargin(margin, rect, gap)), window.innerHeight - height - margin) /* v8 ignore stop */ setPosition({ left, top }) } diff --git a/packages/client/ui-primitives/tests/atoms.client.spec.tsx b/packages/client/ui-primitives/tests/atoms.client.spec.tsx index 3f56fda81c..8b198e4365 100644 --- a/packages/client/ui-primitives/tests/atoms.client.spec.tsx +++ b/packages/client/ui-primitives/tests/atoms.client.spec.tsx @@ -157,6 +157,21 @@ describe('Input', () => { }) }) +describe('MenuItemButton', () => { + it('exposes the current page without preventing activation and clears it when selection moves', () => { + const onSelect = vi.fn() + const { rerender } = render(Notes) + const row = screen.getByRole('menuitem', { name: 'Notes' }) + expect(row.getAttribute('aria-current')).toBe('page') + fireEvent.click(row) + expect(onSelect).toHaveBeenCalledOnce() + rerender(Notes) + expect(row.hasAttribute('aria-current')).toBe(false) + rerender(Notes) + expect(row.hasAttribute('aria-current')).toBe(false) + }) +}) + describe('Menu', () => { const items = [ { id: 'a', label: 'Alpha' }, diff --git a/packages/client/ui-primitives/tests/hover-card.client.spec.tsx b/packages/client/ui-primitives/tests/hover-card.client.spec.tsx index 0f8a67c08b..ddc6718159 100644 --- a/packages/client/ui-primitives/tests/hover-card.client.spec.tsx +++ b/packages/client/ui-primitives/tests/hover-card.client.spec.tsx @@ -243,7 +243,7 @@ describe('HoverCard', () => { expect(vi.getTimerCount()).toBe(0) }) - it('keeps previews below the desktop frame clearance', () => { + it('keeps previews clear of the frame top strip except below an anchor inside it', () => { const prior = document.documentElement.style.getPropertyValue('--dsh-frame-top-clearance') const priority = document.documentElement.style.getPropertyPriority('--dsh-frame-top-clearance') document.documentElement.style.setProperty('--dsh-frame-top-clearance', '100px') @@ -258,6 +258,10 @@ describe('HoverCard', () => { fireEvent(window, new Event('resize')) expect(card.style.top).toBe('120px') expect(card.style.maxHeight).toBe('322px') + // An anchor inside the top strip keeps its preview directly below it. + wrapper.getBoundingClientRect = () => DOMRect.fromRect({ x: 100, y: 10, width: 600, height: 32 }) + fireEvent(window, new Event('resize')) + expect(card.style.top).toBe('50px') } finally { if (prior === '') document.documentElement.style.removeProperty('--dsh-frame-top-clearance') else document.documentElement.style.setProperty('--dsh-frame-top-clearance', prior, priority) diff --git a/packages/client/ui-primitives/tests/menu-top-clearance.client.spec.tsx b/packages/client/ui-primitives/tests/menu-top-clearance.client.spec.tsx new file mode 100644 index 0000000000..6e1700e7f9 --- /dev/null +++ b/packages/client/ui-primitives/tests/menu-top-clearance.client.spec.tsx @@ -0,0 +1,55 @@ +// @vitest-environment jsdom +/** Portal menus keep the desktop frame's top clearance without leaving a gap below an anchor inside it. */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { cleanup, render } from '@testing-library/react' +import { Menu } from '../src/Menu.tsx' + +const root = document.documentElement +let prior = '' + +beforeEach(() => { + prior = root.style.getPropertyValue('--dsh-frame-top-clearance') + root.style.setProperty('--dsh-frame-top-clearance', '48px') +}) + +afterEach(() => { + cleanup() + vi.restoreAllMocks() + if (prior === '') root.style.removeProperty('--dsh-frame-top-clearance') + else root.style.setProperty('--dsh-frame-top-clearance', prior) +}) + +/** + * Open an 80px-tall portal menu from an anchor with the stated rect. + * @param anchor - the anchor rect in viewport coordinates. + * @param side - which anchor edge the menu opens from. + * @returns the render result. + */ +function openMenu(anchor: DOMRect, side: 'top' | 'bottom') { + vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue(anchor) + vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(120) + vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(80) + return render(Open} items={[{ id: 'one', label: 'One' }]} onClose={vi.fn()} />) +} + +describe('Menu top clearance', () => { + it('opens directly below an anchor that sits inside the top strip', () => { + const view = openMenu(new DOMRect(300, 14, 18, 22), 'bottom') + expect(view.getByRole('menu').style.top).toBe('40px') + }) + + it('keeps a menu that does not fit above its anchor below the top strip', () => { + const view = openMenu(new DOMRect(300, 100, 18, 22), 'top') + expect(view.getByRole('menu').style.top).toBe('68px') + }) + + it('places a menu that does not fit above an anchor inside the top strip directly below it', () => { + const view = openMenu(new DOMRect(300, 14, 18, 22), 'top') + expect(view.getByRole('menu').style.top).toBe('40px') + }) + + it('keeps the top strip clear when the anchor has scrolled above the viewport', () => { + const view = openMenu(new DOMRect(300, -30, 18, 22), 'bottom') + expect(view.getByRole('menu').style.top).toBe('68px') + }) +}) diff --git a/packages/client/ui-primitives/tests/tooltip.client.spec.tsx b/packages/client/ui-primitives/tests/tooltip.client.spec.tsx index e4b7891e30..b2e445e080 100644 --- a/packages/client/ui-primitives/tests/tooltip.client.spec.tsx +++ b/packages/client/ui-primitives/tests/tooltip.client.spec.tsx @@ -765,6 +765,30 @@ describe('Tooltip', () => { expect(screen.getByRole('tooltip')).toBeTruthy() }) + it('shows a clipped-only bubble when the anchor or a descendant overflows at show time', () => { + render( + + + , + ) + const anchor = screen.getByRole('button') + const label = screen.getByText('Full…') + const overflowing = new Set() + vi.spyOn(Element.prototype, 'scrollWidth', 'get').mockImplementation(function (this: Element) { return overflowing.has(this) ? 120 : 80 }) + vi.spyOn(Element.prototype, 'clientWidth', 'get').mockReturnValue(80) + fireEvent.pointerEnter(anchor) + expect(screen.queryByRole('tooltip')).toBeNull() + fireEvent.pointerLeave(anchor) + overflowing.add(label) + fireEvent.pointerEnter(anchor) + expect(screen.getByRole('tooltip').textContent).toBe('Full label') + fireEvent.pointerLeave(anchor) + overflowing.clear() + overflowing.add(anchor) + fireEvent.pointerEnter(anchor) + expect(screen.getByRole('tooltip').textContent).toBe('Full label') + }) + it('mouse leave hides the bubble immediately, even while the anchor stays focused', () => { render( diff --git a/packages/client/ui-primitives/tests/use-anchored-position.client.spec.tsx b/packages/client/ui-primitives/tests/use-anchored-position.client.spec.tsx index a3083af168..5dfb8d1e7e 100644 --- a/packages/client/ui-primitives/tests/use-anchored-position.client.spec.tsx +++ b/packages/client/ui-primitives/tests/use-anchored-position.client.spec.tsx @@ -51,10 +51,12 @@ function stubResizeObserver(): Recorded[] { * @param props - whether the panel is open. * @returns the anchor and, while open, the panel carrying the position. */ -function Host({ open, align }: { open: boolean; align?: 'start' | 'end' }) { +function Host({ open, align, side }: { open: boolean; align?: 'start' | 'end'; side?: 'top' | 'bottom' }) { const anchorRef = useRef(null) const panelRef = useRef(null) - const position = useAnchoredPosition({ open, anchorRef, panelRef, gap: 4, margin: 12, ...align === undefined ? {} : { align } }) + const position = useAnchoredPosition({ + open, anchorRef, panelRef, gap: 4, margin: 12, ...align === undefined ? {} : { align }, ...side === undefined ? {} : { side }, + }) return ( <> @@ -64,7 +66,11 @@ function Host({ open, align }: { open: boolean; align?: 'start' | 'end' }) { } describe('useAnchoredPosition', () => { - it.each([{ fullscreen: false, top: 60 }, { fullscreen: true, top: 20 }])('keeps an anchored panel at $top with fullscreen=$fullscreen', ({ fullscreen, top }) => { + it.each([ + { name: 'a panel that does not fit above its anchor', side: 'top', anchor: new DOMRect(100, 100, 200, 10), fullscreen: false, top: 60 }, + { name: 'a panel that does not fit above its anchor', side: 'top', anchor: new DOMRect(100, 100, 200, 10), fullscreen: true, top: 20 }, + { name: 'a panel below an anchor inside the top strip', side: 'bottom', anchor: new DOMRect(100, 0, 200, 10), fullscreen: false, top: 14 }, + ] as const)('keeps $name at $top with fullscreen=$fullscreen', ({ side, anchor, fullscreen, top }) => { const root = document.documentElement const previous = root.getAttribute('style') const priorFullscreen = root.getAttribute('data-fullscreen') @@ -77,10 +83,10 @@ describe('useAnchoredPosition', () => { }) root.style.setProperty('--dsh-frame-top-clearance', '40px') root.toggleAttribute('data-fullscreen', fullscreen) - vi.spyOn(HTMLButtonElement.prototype, 'getBoundingClientRect').mockReturnValue(new DOMRect(100, 0, 200, 10)) + vi.spyOn(HTMLButtonElement.prototype, 'getBoundingClientRect').mockReturnValue(anchor) vi.spyOn(HTMLDivElement.prototype, 'offsetHeight', 'get').mockReturnValue(400) vi.stubGlobal('innerHeight', 600) - const { getByTestId } = render() + const { getByTestId } = render() expect(getByTestId('panel').style.top).toBe(`${top}px`) }) diff --git a/packages/client/ui-reference/locale/de.json b/packages/client/ui-reference/locale/de.json new file mode 100644 index 0000000000..caffa08eac --- /dev/null +++ b/packages/client/ui-reference/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Datei- und Sitzungsreferenzschnittstelle", + "description": "Verweisen Sie auf Dateien oder andere Sitzungen über @ im Eingabefeld." + } +} diff --git a/packages/client/ui-reference/locale/es.json b/packages/client/ui-reference/locale/es.json new file mode 100644 index 0000000000..c82ce82b70 --- /dev/null +++ b/packages/client/ui-reference/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de referencia de archivos y sesiones", + "description": "Archivos de referencia u otras sesiones a través de @ en el cuadro de entrada." + } +} diff --git a/packages/client/ui-reference/locale/fr.json b/packages/client/ui-reference/locale/fr.json new file mode 100644 index 0000000000..392efe7eb4 --- /dev/null +++ b/packages/client/ui-reference/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de référence de fichiers et de sessions", + "description": "Référencez des fichiers ou d'autres sessions via @ dans la zone de saisie." + } +} diff --git a/packages/client/ui-reference/locale/pt.json b/packages/client/ui-reference/locale/pt.json new file mode 100644 index 0000000000..25bd646b1e --- /dev/null +++ b/packages/client/ui-reference/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de referência de arquivo e sessão", + "description": "Faça referência a arquivos ou outras sessões via @ na caixa de entrada." + } +} diff --git a/packages/client/ui-reference/locale/ru.json b/packages/client/ui-reference/locale/ru.json new file mode 100644 index 0000000000..80c1d5d22f --- /dev/null +++ b/packages/client/ui-reference/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Справочный интерфейс файлов и сеансов", + "description": "Ссылайтесь на файлы или другие сеансы через @ в поле ввода." + } +} diff --git a/packages/client/ui-reference/src/client/index.ts b/packages/client/ui-reference/src/client/index.ts index 4cfd6e8677..31e06bb0b8 100644 --- a/packages/client/ui-reference/src/client/index.ts +++ b/packages/client/ui-reference/src/client/index.ts @@ -33,7 +33,7 @@ import { formatFileMention } from '@deepseek-ai/dsh-file-reference/grammar' import type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types' import type { SessionReferenceMentionCandidate } from '@deepseek-ai/dsh-session-reference/types' import { abbreviateHomePath, fileAddressFor } from '@deepseek-ai/dsh-util-workspace-path' -import { en, ja, NS, zh, type ReferenceKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type ReferenceKey } from './locales.ts' declare module '@deepseek-ai/dsh-api-session-controller/client' { interface SessionReferenceSourceMap { @@ -53,7 +53,7 @@ export const inject = [ * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-reference: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-reference: dictionaries') const t = ctx.locale.bind(NS) const sessions = ctx.get('sessions') as ISessions ctx.uiConversation.userText.register(fileReferenceRule) diff --git a/packages/client/ui-reference/src/client/locales.ts b/packages/client/ui-reference/src/client/locales.ts index af8e37175a..a742c82eca 100644 --- a/packages/client/ui-reference/src/client/locales.ts +++ b/packages/client/ui-reference/src/client/locales.ts @@ -77,3 +77,98 @@ export const ja = { 'time.months': '{n}か月', 'time.years': '{n}年', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'section.files': 'Archivos y carpetas', + 'section.subagents': 'Subagentes', + 'section.sessions': 'Sesiones', + 'candidate.noCwd': '(sin cwd)', + 'crumb.root': 'Espacio de trabajo', + 'fileOpen.title': 'No se pudo abrir el archivo', + 'fileOpen.failed': 'No se pudo abrir "{path}". Inténtalo de nuevo o cierra este mensaje.', + 'fileOpen.close': 'Cerrar', + 'fileOpen.retry': 'Reintentar', + 'time.now': 'ahora', + 'time.minutes': '{n}min', + 'time.hours': '{n}h', + 'time.days': '{n}d', + 'time.months': '{n}mo', + 'time.years': '{n}y', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'section.files': 'Fichiers et dossiers', + 'section.subagents': 'Sous-agents', + 'section.sessions': 'Séances', + 'candidate.noCwd': '(pas de cwd)', + 'crumb.root': 'Espace de travail', + 'fileOpen.title': "Impossible d'ouvrir le fichier", + 'fileOpen.failed': "Impossible d'ouvrir \"{path}\". Réessayez ou fermez ce message.", + 'fileOpen.close': 'Fermer', + 'fileOpen.retry': 'Réessayer', + 'time.now': 'maintenant', + 'time.minutes': '{n}min', + 'time.hours': '{n}h', + 'time.days': '{n}d', + 'time.months': '{n}mo', + 'time.years': '{n}y', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'section.files': 'Dateien und Ordner', + 'section.subagents': 'Subagenten', + 'section.sessions': 'Sitzungen', + 'candidate.noCwd': '(kein CWD)', + 'crumb.root': 'Arbeitsbereich', + 'fileOpen.title': 'Datei konnte nicht geöffnet werden', + 'fileOpen.failed': '„{path}“ konnte nicht geöffnet werden. Versuchen Sie es erneut oder schließen Sie diese Nachricht.', + 'fileOpen.close': 'Schließen', + 'fileOpen.retry': 'Erneut versuchen', + 'time.now': 'jetzt', + 'time.minutes': '{n}min', + 'time.hours': '{n}h', + 'time.days': '{n}d', + 'time.months': '{n}mo', + 'time.years': '{n}y', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'section.files': 'Файлы и папки', + 'section.subagents': 'Субагенты', + 'section.sessions': 'Сессии', + 'candidate.noCwd': '(без КДВ)', + 'crumb.root': 'Рабочая область', + 'fileOpen.title': 'Не удалось открыть файл', + 'fileOpen.failed': 'Не удалось открыть «{path}». Попробуйте еще раз или закройте это сообщение.', + 'fileOpen.close': 'Закрыть', + 'fileOpen.retry': 'Повторить', + 'time.now': 'сейчас', + 'time.minutes': '{n}мин', + 'time.hours': '{n}h', + 'time.days': '{n}d', + 'time.months': '{n}mo', + 'time.years': '{n}y', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'section.files': 'Arquivos e pastas', + 'section.subagents': 'Subagentes', + 'section.sessions': 'Sessões', + 'candidate.noCwd': '(sem cwd)', + 'crumb.root': 'Espaço de trabalho', + 'fileOpen.title': 'Não foi possível abrir o arquivo', + 'fileOpen.failed': 'Não foi possível abrir “{path}”. Tente novamente ou feche esta mensagem.', + 'fileOpen.close': 'Fechar', + 'fileOpen.retry': 'Tentar novamente', + 'time.now': 'agora', + 'time.minutes': '{n}min', + 'time.hours': '{n}h', + 'time.days': '{n}d', + 'time.months': '{n} mês', + 'time.years': '{n}s', +} satisfies Record diff --git a/packages/client/ui-renderer/locale/de.json b/packages/client/ui-renderer/locale/de.json new file mode 100644 index 0000000000..d913a630cf --- /dev/null +++ b/packages/client/ui-renderer/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstellen-Rendering", + "description": "Integrieren Sie die von jedem Plug-In bereitgestellten Schnittstellenkomponenten in die Anwendung." + } +} diff --git a/packages/client/ui-renderer/locale/es.json b/packages/client/ui-renderer/locale/es.json new file mode 100644 index 0000000000..74b778c2c5 --- /dev/null +++ b/packages/client/ui-renderer/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Representación de interfaz", + "description": "Ensamble los componentes de la interfaz proporcionados por cada complemento en la aplicación." + } +} diff --git a/packages/client/ui-renderer/locale/fr.json b/packages/client/ui-renderer/locale/fr.json new file mode 100644 index 0000000000..40968cbcdf --- /dev/null +++ b/packages/client/ui-renderer/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Rendu d'interface", + "description": "Assemblez les composants d’interface fournis par chaque plug-in dans l’application." + } +} diff --git a/packages/client/ui-renderer/locale/pt.json b/packages/client/ui-renderer/locale/pt.json new file mode 100644 index 0000000000..b5d9f75753 --- /dev/null +++ b/packages/client/ui-renderer/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Renderização de interface", + "description": "Monte os componentes de interface fornecidos por cada plug-in no aplicativo." + } +} diff --git a/packages/client/ui-renderer/locale/ru.json b/packages/client/ui-renderer/locale/ru.json new file mode 100644 index 0000000000..09d2f66a5b --- /dev/null +++ b/packages/client/ui-renderer/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Рендеринг интерфейса", + "description": "Соберите в приложение компоненты интерфейса, предоставляемые каждым плагином." + } +} diff --git a/packages/client/ui-schedule/README.i18n.yaml b/packages/client/ui-schedule/README.i18n.yaml index b1cb01557a..6af57d26c8 100644 --- a/packages/client/ui-schedule/README.i18n.yaml +++ b/packages/client/ui-schedule/README.i18n.yaml @@ -10,19 +10,19 @@ zh: eb3e5d5a802da592 /deepseek-ai-dsh-client-ui-schedule/summary: en: 8dfc371c14bb4415 - zh: 9b3545c1a7468a7a + zh: 5e2fa588983524ed /deepseek-ai-dsh-client-ui-schedule/table-of-contents: en: d152484eb41ac6b4 zh: 405b30feaf48c087 /deepseek-ai-dsh-client-ui-schedule/use-this-package: - en: babac791422f1b8f - zh: f633dbfc1b7bb4bd + en: fccffd577fcf93f2 + zh: 4719236f7d89e0a8 /deepseek-ai-dsh-client-ui-schedule/use-this-package/time-zone-data-source: en: 47cd16c142e82385 zh: 181849a3c7a167fd /deepseek-ai-dsh-client-ui-schedule/understand-the-implementation: en: 959145f9a08c3913 - zh: caae642a82d2acd3 + zh: 7c0afb742790c474 /deepseek-ai-dsh-client-ui-schedule/further-exploration: en: 06e265120b05dbf5 zh: 08ab1ef979b8ea40 diff --git a/packages/client/ui-schedule/README.md b/packages/client/ui-schedule/README.md index 6593c58787..ee91c4aa00 100644 --- a/packages/client/ui-schedule/README.md +++ b/packages/client/ui-schedule/README.md @@ -25,9 +25,9 @@ Manage active and inactive tasks from the Automation tasks page and the right Si ## Use this package -The shipped Web composition mounts `ui-schedule` with the Host Schedule service. The clock context that supplies the model's current time is preset-owned: `standard`, `cordis`, and `ptc` declare it, and `minimal` declares neither it nor the reminder tools. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Its task list supports text search and one All, Enabled, and Inactive status filter row; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state shows a decorative document-and-clock illustration above its notice and New action, with colors that follow the light or dark theme. Selecting an active or inactive task opens a second detail level with Rules and Records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. When the strip cannot fit the tab list beside the labelled entry, the entry becomes an icon button with the same button style as More task actions, with the Session chat icon and a Linked session tooltip that also shows while the entry is disabled; the entry keeps focus when it switches layouts; the tab list scrolls without a visible scrollbar. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation. +The shipped Web composition mounts `ui-schedule` with the Host Schedule service. The clock context that supplies the model's current time is preset-owned: `standard`, `cordis`, and `ptc` declare it, and `minimal` declares neither it nor the reminder tools. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Once the catalog holds an inactive task, the search row's More task actions menu offers Clear all inactive tasks, which removes every inactive task after a confirmation that states how many inactive tasks the catalog currently holds. The page's task list supports text search and one Enabled, Inactive, and All status filter row, starting on Enabled; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state shows a decorative document-and-clock illustration above its notice and New action, with colors that follow the light or dark theme. Selecting an active or inactive task opens a second detail level with Rules and Records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. When the strip cannot fit the tab list beside the labelled entry, the entry becomes an icon button with the same button style as More task actions, with the Session chat icon and a Linked session tooltip that also shows while the entry is disabled; the entry keeps focus when it switches layouts; the tab list scrolls without a visible scrollbar. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation. -The page header keeps the title at its natural width and moves New to another line when the two no longer fit together. The action label stays on one line. On a coarse pointer the list's search box and the detail's instruction and run-time fields take a 16px step — iOS Safari zooms the visual viewport for a focused field below that size — while the name control keeps its 20px step and a fine pointer keeps the compact steps. +The page header keeps the title at its natural width and moves the actions to another line when the title and the actions no longer fit together. Each action label stays on one line. On a coarse pointer the list's search box and the detail's instruction and run-time fields take a 16px step — iOS Safari zooms the visual viewport for a focused field below that size — while the name control keeps its 20px step and a fine pointer keeps the compact steps. Selecting a task from the list opens Rules; refreshing that task preserves the selected tab. Rules edits an active task's name, instruction, and run time; the edits stay in a local draft until saved, and inactive tasks remain read-only. Unmodified Left/Right arrows cycle the tabs, Home/End select the first/last tab, and keyboard focus follows the selection. The tabs do not consume these keys with Alt, Ctrl, Meta, or Shift. The linked-Session control in the detail tab strip stays available on the Rules tab, and confirmed deletion is available from either view. @@ -41,7 +41,7 @@ After the last saved page loads, a nonempty history with confirmed pruning shows A successful empty history page is distinct from loading, query failure, a missing task, or an invalid cursor. Failed requests retain previously loaded records with a warning and Retry delivery records; an invalid cursor offers Refresh delivery records to reload the newest page. Changing the latest `messageId` refreshes the newest page without switching tabs. Responses superseded by a refresh or received after changing tasks, leaving Records, or closing details do not replace the current view. -The Automation tasks page requires confirmation before deleting a task. Deletion is hard: the Host removes the stored task row together with its saved delivery records, so the task stops future delivery, leaves `list` and `catalog`, and its delivery history is no longer readable. Deletion leaves the original Session and queued messages intact, and the list keeps the row visible only until an authoritative query confirms the removal. Every deletion path — the Tasks page detail, the Session task tab, and the header popover — reports its settled outcome into one store that the app-wide `shell.overlay` notice renders, so the outcome outlives the panel or tab that asked for it: the notice reads `Task deleted.` once the Host removed the row with its saved delivery records, and `Could not delete the task.` after a failure, which leaves the rule in place to retry. Confirming a deletion closes the detail or the tab it was confirmed from, and each notice is keyed by its own sequence so a later one restarts the banner instead of being folded into an earlier one. A query failure keeps the last known records and shows a separate Retry action; it never appears as an empty successful result. A one-shot remains stored as inactive after durable inbox delivery. Its details and original Session id remain available through the All or Inactive filter until explicit deletion. The latest delivery shows its occurrence time; the message id and delivery acknowledgment time are not shown. Neither inactive status nor that receipt means model execution completed. +The Automation tasks page requires confirmation before deleting a task. Deletion is hard: the Host removes the stored task row together with its saved delivery records, so the task stops future delivery, leaves `list` and `catalog`, and its delivery history is no longer readable. Deletion leaves the original Session and queued messages intact, and the list keeps the row visible only until an authoritative query confirms the removal. Every deletion path — the Tasks page detail, the Session task tab, the header popover, and the Tasks page's bulk cleanup — reports its settled outcome into one store that the app-wide `shell.overlay` notice renders, so the outcome outlives the panel or tab that asked for it: the notice reads `Task deleted.` once the Host removed the row with its saved delivery records, and `Could not delete the task.` after a failure, which leaves the rule in place to retry. The same notice reports a bulk cleanup: `Cleared N inactive tasks.` with the count the Host answered, `No inactive tasks to clear.` when the Host reports that it removed none, and `Could not clear tasks.` after a failure; a normal failure leaves the remaining inactive rows for a retry, while a backend `unknown-state` failure may have removed some rows and quarantines the Host's schedule domain until it is reopened. The bulk cleanup closes only its confirmation, whereas confirming a single deletion closes the detail or the tab it was confirmed from. Each notice is keyed by its own sequence so a later one restarts the banner instead of being folded into an earlier one. A query failure keeps the last known records and shows a separate Retry action; it never appears as an empty successful result. A one-shot remains stored as inactive after durable inbox delivery. Its details and original Session id remain available through the All or Inactive filter until explicit deletion. The latest delivery shows its occurrence time; the message id and delivery acknowledgment time are not shown. Neither inactive status nor that receipt means model execution completed. An open Session renders an icon-only clock button among its header's right-aligned utilities, immediately left of the overflow menu, only once it has something to open: the first read mounts nothing, a read that returns no active reminder leaves the header without the button, a refresh keeps the last known reminders visible, and a failed read keeps the button with its retry. That rule prevents the clock from appearing and disappearing around the first answer. Its accessible name states the reminder count when the read is ready and the generic reminder label otherwise. With exactly one active reminder, the button opens that task's detail in the right Sidebar directly. With any other count it opens a popover that lists overdue reminders first, then future reminders by target time, with stored title, frequency, browser-local target time, relative time, and a Delete button; the title is a button that opens that task's detail in the right Sidebar. The task tab names the shown task's stored title on its chip and carries the same rule, delivery records, name, instruction, and run-time editing, confirmed deletion, and original-Session link as the Automation tasks page; it never activates a Session. Daily frequency text retains the saved local time and zone, and every next-target instant is shown as a device-zone clock with its relative duration in parentheses; the rule's own zone appears only in that frequency text. Escape closes the popover and returns focus to the button; an outside pointer press dismisses it. Deleting the final reminder removes the button, and an already open popover stays expandable until then. diff --git a/packages/client/ui-schedule/README.zh.md b/packages/client/ui-schedule/README.zh.md index 8ac26ee077..c89225104e 100644 --- a/packages/client/ui-schedule/README.zh.md +++ b/packages/client/ui-schedule/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -在「自动化任务」页面和右侧栏的任务页签中管理各个会话的活动和未运行定时任务:搜索、筛选、修改名称、指令和运行时间、浏览任务运行记录、删除任务,或打开原会话。每个界面都用任务已存储的标题命名任务。已打开会话在存在活动提醒时,页头显示仅图标的提醒时钟,点击在右侧栏打开该任务详情;`schedule_create` 调用把创建的任务渲染为会话卡片;侧栏中带活动任务的未归档空闲会话行显示时钟标记,其悬停卡片列出这些任务。 +在「自动化任务」页面和右侧栏的任务页签中管理各个会话的活动和已结束定时任务:搜索、筛选、修改名称、指令和运行时间、浏览任务运行记录、删除任务,或打开原会话。每个界面都用任务已存储的标题命名任务。已打开会话在存在活动提醒时,页头显示仅图标的提醒时钟,点击在右侧栏打开该任务详情;`schedule_create` 调用把创建的任务渲染为会话卡片;侧栏中带活动任务的未归档空闲会话行显示时钟标记,其悬停卡片列出这些任务。 ## 目录 @@ -25,15 +25,15 @@ kind: "package-reference" ## 使用本包 -随发行版交付的 Web 组合把 `ui-schedule` 与 Host Schedule 服务一起挂载。为模型提供当前时间的时钟上下文归 preset 所有:`standard`、`cordis` 与 `ptc` 声明它,`minimal` 既不声明它,也不声明提醒工具。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。列表支持文本搜索和一行“全部/已开启/未运行”状态筛选;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示和“新建”操作上方显示装饰性的文档与时钟插画,配色跟随浅色或深色主题。选择活动或未运行任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。页签条放不下页签列表和带文字的入口时,入口收为与“更多任务操作”同样式的图标按钮,使用会话对话图标,悬停提示“关联会话”,入口禁用时也会显示;切换形态时保留焦点;页签列表可滚动但不显示滚动条。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。 +随发行版交付的 Web 组合把 `ui-schedule` 与 Host Schedule 服务一起挂载。为模型提供当前时间的时钟上下文归 preset 所有:`standard`、`cordis` 与 `ptc` 声明它,`minimal` 既不声明它,也不声明提醒工具。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。目录中存在已结束任务后,搜索行右侧的「更多任务操作」菜单提供「清除全部已结束任务」,经确认后移除全部已结束任务,确认提示说明目录当前包含多少个已结束任务。列表支持文本搜索和一行“已开启/已结束/全部”状态筛选,默认停在“已开启”;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示和“新建”操作上方显示装饰性的文档与时钟插画,配色跟随浅色或深色主题。选择活动或已结束任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。页签条放不下页签列表和带文字的入口时,入口收为与“更多任务操作”同样式的图标按钮,使用会话对话图标,悬停提示“关联会话”,入口禁用时也会显示;切换形态时保留焦点;页签列表可滚动但不显示滚动条。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。 -页头按标题的自然宽度排布;标题和“新建”无法并排容纳时,将按钮移到下一行。操作文案保持单行。粗指针设备上,列表的搜索框以及详情的指令和运行时间输入框采用 16px 字号——iOS Safari 会放大聚焦到更小字号输入框时的视觉视口——而名称输入框保留 20px、细指针设备保留紧凑字号。 +页头按标题的自然宽度排布;标题和操作无法并排容纳时,将操作移到下一行。每个操作文案保持单行。粗指针设备上,列表的搜索框以及详情的指令和运行时间输入框采用 16px 字号——iOS Safari 会放大聚焦到更小字号输入框时的视觉视口——而名称输入框保留 20px、细指针设备保留紧凑字号。 -从列表选择任务时默认打开“规则”;刷新同一任务会保留当前页签。活动任务的“规则”修改名称、指令和运行时间;这些修改保留在本地草稿中直到保存,未运行任务保持只读。不带修饰键的左右方向键循环切换页签,Home/End 选择第一个/最后一个页签,键盘焦点随选择移动。页签不会拦截这些键与 Alt、Ctrl、Meta 或 Shift 的组合。共用的会话入口位于详情页签条内,在“规则”页签可见;确认删除操作在两种页签下都可用。 +从列表选择任务时默认打开“规则”;刷新同一任务会保留当前页签。活动任务的“规则”修改名称、指令和运行时间;这些修改保留在本地草稿中直到保存,已结束任务保持只读。不带修饰键的左右方向键循环切换页签,Home/End 选择第一个/最后一个页签,键盘焦点随选择移动。页签不会拦截这些键与 Alt、Ctrl、Meta 或 Shift 的组合。共用的会话入口位于详情页签条内,在“规则”页签可见;确认删除操作在两种页签下都可用。 “规则”在“运行时间”卡片中以行编辑本地草稿:“重复”是含每周、周一至周五、每天、每 N 小时、每 N 分钟、每 N 秒、仅一次和自定义(cron)的菜单,每周规则增加“星期”行,固定速率选项只修改它所属单位的间隔数量,一次性规则用独立的“日期”和“时间”两行编辑目标,自定义(cron)用一行“Cron 表达式”编辑按所选时区解释的 5 字段表达式。卡片在浏览器中解析该表达式,并在该行下方显示描述它的本地化语句;Host 会拒绝的表达式改为显示本地化校验信息,并在解析通过前阻止保存。已存储的每周规则若星期集合恰好为周一至周五,则选中“周一至周五”选项。“日期”行是只读触发器,以 `YYYY/MM/DD` 显示暂存的日期并展开带本地化星期表头、月份切换和今天标记的月历;草稿与提交文本保持 ISO 形态;“时间”行是只读触发器,显示统一的 24 小时时钟,并展开时、分、秒三列可滚动列表,当前值高亮并在打开时滚动到可见。在月历或时钟上点选即按与其他行相同的分阶段保存写入草稿;方向键在打开的列或网格内移动,Enter 选定当前值,Escape 关闭面板并把焦点还给所在行,被禁用的行无法打开面板。时钟行显示到秒,未改动的行在提交时保留已存储的毫秒值,编辑时间时按整秒精度提交。“时区”使用高度受限的可搜索菜单。桌面端打开时自动聚焦搜索框;手机和平板需要手动聚焦。顶部无标题区域保留浏览器本地最近选过的 5 个准确时区;首次使用按真实系统时区和 UTC 初始化,而不根据界面语言猜测所在地。系统时区带有标记,其余清单按当前 UTC 偏移、再按规范 IANA 标识排序。可见文案把 `UTC±HH:MM` 放在本地化时区名称之前;IANA 标识仅在内部使用,但可用于搜索。ICU 显示成相同偏移和本地化名称的多个时区只占一行;所有被合并的标识仍可用于搜索,任务已保存的标识也会继续作为选中值,不会被改写。时区清单来自 `Intl.supportedValuesOf('timeZone')`,名称来自 `Intl.DateTimeFormat(..., { timeZoneName: 'longGeneric' })`:两者都是浏览器内置的 ICU/CLDR 数据,不是抓取或产品自维护的翻译表。因此没有需要刷新的爬虫或生成时区资源;第三方语言包只需提供常规的 `time.locale` 值,即可自动获得对应的 ICU 名称,仅需翻译选择器周边的 UI 文案。运行时无法枚举时改用简短的标识候选列表,已存储但不在其中的时区会保留。每天、每周和 cron 保留各自已存储的规则与时区,其频率行仅在时区与宿主系统时区不同时标出时区。一次性 After/At 以记录已存储的时区初始化日期和时间;记录未保存时区时取本设备时区,初始钟表值在该时区下仍表示同一瞬时;更改时区会保留输入的钟表值并改变时点。因选择种类而重新初始化的种类使用已提交发生时点的钟表值和该选项的时区,该时区取记录已存储的时区或本设备时区:每周的星期取自该发生时点在该时区下的星期,cron 则把该发生时点的钟表值写成每天执行的表达式。固定间隔至少为 60 整秒,不受时区影响。一次保存以分阶段的变更提交完整预期记录,并保留 id、原会话、最近一次回执及已保存历史;未来目标选择、重新确定起算时间和未变更规则的无写入语义见 [Schedule](../../schedule/schedule/README.zh.md#use-this-package)。 -行在保存进行中、目录加载中、删除期间以及任务未运行时禁用。仅当本地草稿与已存储任务不同时才出现保存栏:显示“有未保存的修改”及“取消”和“保存修改”,保存期间“保存修改”变为“保存中…”。“取消”恢复已存储的值。仅当草稿没有未保存修改时,目录刷新才会成为显示值,因此未保存的草稿会保留到权威刷新之后。保存失败保留草稿并显示本地化提示;冲突后针对刷新后的记录重试。任务从目录消失时,“删除”会禁用,直到任务重新出现。更新成功后刷新权威目录。更新被拒绝或无法确认与目录读取失败相互区分:“重试”重新加载目录,不将该更新显示为失败。若更新本身无法确认,应在重试前检查任务。切换任务或关闭详情后才到达的响应不会替换当前视图,任何失败都不会回滚宿主已开始的写入。 +行在保存进行中、目录加载中、删除期间以及任务已结束时禁用。仅当本地草稿与已存储任务不同时才出现保存栏:显示“有未保存的修改”及“取消”和“保存修改”,保存期间“保存修改”变为“保存中…”。“取消”恢复已存储的值。仅当草稿没有未保存修改时,目录刷新才会成为显示值,因此未保存的草稿会保留到权威刷新之后。保存失败保留草稿并显示本地化提示;冲突后针对刷新后的记录重试。任务从目录消失时,“删除”会禁用,直到任务重新出现。更新成功后刷新权威目录。更新被拒绝或无法确认与目录读取失败相互区分:“重试”重新加载目录,不将该更新显示为失败。若更新本身无法确认,应在重试前检查任务。切换任务或关闭详情后才到达的响应不会替换当前视图,任何失败都不会回滚宿主已开始的写入。 打开“任务运行记录”时才请求最新的 20 条已保存记录。“加载更早的记录”按追加顺序从新到旧增加一页,不受实际时间回拨影响。该视图中页签条就是整个页头:名称输入框和“下次运行”行只属于“规则”。每条“已保存的任务运行记录”以时钟图标和任务时区下的本地月份名与时钟字段开头(一次性记录使用浏览器时区),并在存在时显示实际保存的提示文本。超过两行的提示文本只显示前两行并提供“展开”,展开后可“收起”;面板宽度变化时,收起状态的提示文本会重新判断是否超过两行。旧回执没有提示文本时,绝不以当前指令代替。 @@ -41,7 +41,7 @@ kind: "package-reference" 历史查询成功的空页与加载中、查询失败、任务不存在或游标无效相互区分。请求失败时保留此前加载的记录,并显示警告和“重试加载任务运行记录”;游标无效时提供“刷新任务运行记录”,重新加载最新一页。最新 `messageId` 变化时刷新最新一页,不切换页签。被刷新取代的响应,以及切换任务、离开任务运行记录或关闭详情后才收到的响应,不会替换当前视图。 -自动化任务页面要求确认后才能删除任务。删除是硬删除:Host 连同已保存的任务运行记录一起移除存储的任务行,该任务因此停止后续投递、离开 `list` 与 `catalog`,其投递历史不再可读。删除保留原会话和已入队消息;列表仅在权威查询确认移除之前保留该行。每条删除路径——自动化任务页的详情、会话的任务页签、以及页头弹层——都把最终结果写入同一个存储,由应用级 `shell.overlay` 提示渲染,因此提示的生命周期长于发起它的面板或页签:Host 连同已保存的运行记录移除该行后提示显示「任务已删除」,失败时显示「无法删除任务」,并保留规则以便重试。确认删除会关闭确认所在的面板或页签,每条提示按自身序号区分,后一条会重新开始横幅而不是并入前一条。查询失败保留最近已知记录,并显示独立的重试操作;不会呈现为查询成功的空结果。一次性任务在持久收件箱投递后仍以未运行状态保存。显式删除前,通过全部或未运行筛选仍可查看详情与原会话 id。最近一次投递显示其发生时间,不显示消息 id 和投递确认时间;未运行状态和该回执均不表示模型执行完成。 +自动化任务页面要求确认后才能删除任务。删除是硬删除:Host 连同已保存的任务运行记录一起移除存储的任务行,该任务因此停止后续投递、离开 `list` 与 `catalog`,其投递历史不再可读。删除保留原会话和已入队消息;列表仅在权威查询确认移除之前保留该行。每条删除路径——自动化任务页的详情、会话的任务页签、以及页头弹层——都把最终结果写入同一个存储,由应用级 `shell.overlay` 提示渲染,因此提示的生命周期长于发起它的面板或页签:Host 连同已保存的运行记录移除该行后提示显示「任务已删除」,失败时显示「无法删除任务」,并保留规则以便重试。批量清理也由该提示报告:显示 Host 答复的行数「已清理 N 个已结束任务」;Host 答复未移除任何行时显示「没有可清理的已结束任务」;失败时显示「清除任务失败」;普通失败会保留尚未移除的已结束行以便重试,后端报告 `unknown-state` 时可能已移除部分行,并会隔离 Host 的 schedule 领域,直到重新打开。批量清理只关闭自己的确认对话框。确认删除会关闭确认所在的面板或页签,每条提示按自身序号区分,后一条会重新开始横幅而不是并入前一条。查询失败保留最近已知记录,并显示独立的重试操作;不会呈现为查询成功的空结果。一次性任务在持久收件箱投递后仍以已结束状态保存。显式删除前,通过全部或已结束筛选仍可查看详情与原会话 id。最近一次投递显示其发生时间,不显示消息 id 和投递确认时间;已结束状态和该回执均不表示模型执行完成。 已打开的会话只有在确实有可打开的内容时,才在页头右侧功能位中、紧邻更多操作菜单左侧渲染一个仅图标时钟按钮:首次读取前不渲染任何内容,读取结果为没有活动提醒时页头不显示该按钮,刷新期间保留上一次已知的提醒,读取失败则保留按钮及其重试。该规则避免时钟在首次结果前后出现又消失。其无障碍名称在读取就绪时给出提醒数量,否则显示通用提醒标签。当该会话只有一个活动提醒时,按钮直接在右侧栏打开该任务的详情。其余数量都会打开弹层:先列出逾期提醒,再按目标时间列出未来提醒,显示已存储的标题、频率、浏览器本地目标时间、相对时间及删除按钮;标题本身是一个按钮,点击后在右侧栏打开该任务的详情。任务页签在标签上显示当前任务已存储的标题,并提供与自动化任务页面相同的规则、任务运行记录、名称、指令和运行时间修改、确认删除和原会话入口;它不会激活会话。每日频率文本保留保存的本地时间和时区;每个下一目标时点都显示为设备时区的钟点和括号内的相对时长;规则自身的时区只显示在该频率文本中。Escape 关闭弹层并将焦点返回按钮;点击外部将其关闭。删除最后一条提醒会移除按钮;在此之前,已经打开的弹层保持可展开状态。 @@ -49,7 +49,7 @@ kind: "package-reference" 调用 `schedule_create` 时,创建的任务会以卡片形式渲染在 transcript 中,包含时钟图标、已存储的标题、频率,以及打开该任务右侧栏页签的“打开”按钮。卡片先按该调用持久化的结果 JSON 收窄出任务,并从该结果读取 `title`;当一次在卡片出现之后成功结束的目录读取解析到该任务时,卡片改为跟随目录显示当前名称与频率,若该读取中不含该记录,则以“已删除”代替频率显示。在已存储字段存在之前录制的结果回退为指令首行。仍在运行的调用、创建失败,或结果中没有完整任务时,卡片保留该行,并在结果为单个非空文本块时显示原始结果文本,不提供“打开”操作。卡片不增加 Host 字段,也不改变 Session 日志格式,因此早于该卡片录制的 Session 也会渲染它。 -侧栏的 Session 行会标记有定时任务的会话:当某行处于空闲状态、未归档且其会话至少有一个活动任务时,该行显示时钟标记;归档行该格留空,其实时状态只在悬浮卡片中显示。对存在活动任务的会话长按悬停时,无论该行是否空闲,都会在其已有的悬浮卡片内增加自动化区段,最多列出两个任务(图标、已存储的标题、频率与下次运行文本),还有更多任务被隐藏时显示遗漏行。处于更高优先级状态的行——有待处理交互、活动仍在进行,或有未查看的完成提醒——保留自身状态点且不渲染标记,两者不会同时出现。该标记和悬停区段读取共享的活动与未运行 Host 任务目录,并从中筛选本会话的活动任务;它们不会激活、保留或取消归档任何会话,也不读取 Session 日志。 +侧栏的 Session 行会标记有定时任务的会话:当某行处于空闲状态、未归档且其会话至少有一个活动任务时,该行显示时钟标记;归档行该格留空,其实时状态只在悬浮卡片中显示。对存在活动任务的会话长按悬停时,无论该行是否空闲,都会在其已有的悬浮卡片内增加自动化区段,最多列出两个任务(图标、已存储的标题、频率与下次运行文本),还有更多任务被隐藏时显示遗漏行。处于更高优先级状态的行——有待处理交互、活动仍在进行,或有未查看的完成提醒——保留自身状态点且不渲染标记,两者不会同时出现。该标记和悬停区段读取共享的活动与已结束 Host 任务目录,并从中筛选本会话的活动任务;它们不会激活、保留或取消归档任何会话,也不读取 Session 日志。 ----- @@ -65,7 +65,7 @@ kind: "package-reference"
实现内部细节 — 点击展开 -浏览器入口注册 `schedules` 主面板及对应的侧栏入口、Session 页头右侧功能位中位于 `order: -5` 且紧邻更多操作菜单左侧的 `schedule-catalog` 入口、经由 `ctx.uiConversation.events` 与 ui-chat 的 list seat `conversation.chat.turnTail` 在轮次层级注册的 `schedule_create` transcript 卡片(因此它渲染在该轮收尾的 assistant 文本之后,并在工具调用组折叠时仍然可见)、root 作用域的两个 Session 行 seat `sidebar.session.row.leading` 与 `sidebar.session.row.hover`,以及 kind 为 `scheduleTask` 的右侧栏页类型 `@deepseek-ai/dsh-client-ui-schedule/task`。该类型分两阶段注册:先把其定义注册进 `ctx.sidebarRightTabs`,再按该定义的 id 把正文与 chip 注册进带键 seat `sidebar.right.pane.tab` 与 `sidebar.right.pane.tab.title`。从 Session 入口打开任务时调用 `ctx.sidebarRight.openTab`,传入该入口的 Session id 与所选任务 id,详情因此出现在该入口所属 Session 的右侧栏中;自动化任务页面保留自己的列表并共用同一个详情组件。全局页面、任务页签、行标记与行悬停区段都从同一个 `schedule.catalog` 源读取活动与未运行任务;页头入口保留自己的按 Session `schedule.list` 读取,只返回该 Session 的活动任务。每个面都使用同一套可观察的查询生命周期,并在 `schedule/changed` 或连接重置后刷新。transcript 卡片从该调用持久化的结果 JSON 收窄出创建的任务,而不是依赖 Host 追加字段;该调用自身的工具调用组单元格由 ui-tool 的通用键控视图渲染,且卡片只对产生完整任务的调用出现。该对齐是因果判定而非时钟比较:目录快照带有 `readRequest`(最新被请求的读取序号)与 `readSettled`(产出当前记录的读取序号);Turn 尾部挂载时锚定 `readRequest` 并请求一次读取,仅在 `readSettled` 大于该锚点后才用其保留记录判定,且源把一次提交内的请求合并为单次 `list()`,只把该读取共享给尚未观察到它的调用者。比较浏览器时钟与 Host 时钟无法确立这一顺序。共用的删除回调与注入的 `onUpdateTiming` 保留所选记录的原 Session 绑定,而不使用当前打开的对话。详情保留当前任务的本地草稿、用于比较的权威值以及等待中或失败的保存;保存等待中或失败期间以及删除确认后,属主保留所显示的任务,使目录刷新在丢弃该行时既不能关闭已呈现的失败,也不能关闭陈述删除的详情。保存发出一次携带完整预期 `ScheduleRecord` 和分阶段变更的 `schedule.update` 请求;`onUpdateTiming` 刷新目录,而不把回读失败变成修改失败。 +浏览器入口注册 `schedules` 主面板及对应的侧栏入口、Session 页头右侧功能位中位于 `order: -5` 且紧邻更多操作菜单左侧的 `schedule-catalog` 入口、经由 `ctx.uiConversation.events` 与 ui-chat 的 list seat `conversation.chat.turnTail` 在轮次层级注册的 `schedule_create` transcript 卡片(因此它渲染在该轮收尾的 assistant 文本之后,并在工具调用组折叠时仍然可见)、root 作用域的两个 Session 行 seat `sidebar.session.row.leading` 与 `sidebar.session.row.hover`,以及 kind 为 `scheduleTask` 的右侧栏页类型 `@deepseek-ai/dsh-client-ui-schedule/task`。该类型分两阶段注册:先把其定义注册进 `ctx.sidebarRightTabs`,再按该定义的 id 把正文与 chip 注册进带键 seat `sidebar.right.pane.tab` 与 `sidebar.right.pane.tab.title`。从 Session 入口打开任务时调用 `ctx.sidebarRight.openTab`,传入该入口的 Session id 与所选任务 id,详情因此出现在该入口所属 Session 的右侧栏中;自动化任务页面保留自己的列表并共用同一个详情组件。全局页面、任务页签、行标记与行悬停区段都从同一个 `schedule.catalog` 源读取活动与已结束任务;页头入口保留自己的按 Session `schedule.list` 读取,只返回该 Session 的活动任务。每个面都使用同一套可观察的查询生命周期,并在 `schedule/changed` 或连接重置后刷新。transcript 卡片从该调用持久化的结果 JSON 收窄出创建的任务,而不是依赖 Host 追加字段;该调用自身的工具调用组单元格由 ui-tool 的通用键控视图渲染,且卡片只对产生完整任务的调用出现。该对齐是因果判定而非时钟比较:目录快照带有 `readRequest`(最新被请求的读取序号)与 `readSettled`(产出当前记录的读取序号);Turn 尾部挂载时锚定 `readRequest` 并请求一次读取,仅在 `readSettled` 大于该锚点后才用其保留记录判定,且源把一次提交内的请求合并为单次 `list()`,只把该读取共享给尚未观察到它的调用者。比较浏览器时钟与 Host 时钟无法确立这一顺序。共用的删除回调与注入的 `onUpdateTiming` 保留所选记录的原 Session 绑定,而不使用当前打开的对话。详情保留当前任务的本地草稿、用于比较的权威值以及等待中或失败的保存;保存等待中或失败期间以及删除确认后,属主保留所显示的任务,使目录刷新在丢弃该行时既不能关闭已呈现的失败,也不能关闭陈述删除的详情。保存发出一次携带完整预期 `ScheduleRecord` 和分阶段变更的 `schedule.update` 请求;`onUpdateTiming` 刷新目录,而不把回读失败变成修改失败。 刷新恢复出的任务标签没有导航参数,因为 Sidebar 只持久化标签的布局记录,不持久化打开方传入的内容。因此该页面类型自行保存从 Session 加布局记录 id 到该记录最后显示任务的 Session 和 id 的关联:每个 Session 一个 `dsh.schedule.task-tab.v1.` localStorage 键,按 tab id 一条记录,且从不保存任务内容。正文和 chip 只对没有导航参数的记录读取该条目,在标签出现之后成功的一次读取无法解析时将其删除,且不代替关联声称任务缺失。这样的一次读取无法为该标签给出任务标识时,正文陈述该状态,而不呈现永不结束的加载。 diff --git a/packages/client/ui-schedule/locale/de.json b/packages/client/ui-schedule/locale/de.json new file mode 100644 index 0000000000..d3a3f03e6b --- /dev/null +++ b/packages/client/ui-schedule/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Aufgabenschnittstelle", + "description": "Die Seite „Automatisierungsaufgaben“ in der Seitenleiste sowie Aufgabenkarten und Erinnerungsmarkierungen in Sitzungen." + } +} diff --git a/packages/client/ui-schedule/locale/es.json b/packages/client/ui-schedule/locale/es.json new file mode 100644 index 0000000000..360c54c06c --- /dev/null +++ b/packages/client/ui-schedule/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de tareas", + "description": "La página de tareas de automatización en la barra lateral, además de tarjetas de tareas y marcas de recordatorio en las sesiones." + } +} diff --git a/packages/client/ui-schedule/locale/fr.json b/packages/client/ui-schedule/locale/fr.json new file mode 100644 index 0000000000..98209dca10 --- /dev/null +++ b/packages/client/ui-schedule/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface des tâches", + "description": "La page Tâches d'automatisation dans la barre latérale, ainsi que les fiches de tâches et les marques de rappel dans les sessions." + } +} diff --git a/packages/client/ui-schedule/locale/pt.json b/packages/client/ui-schedule/locale/pt.json new file mode 100644 index 0000000000..05e228e1cb --- /dev/null +++ b/packages/client/ui-schedule/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de tarefas", + "description": "A página de tarefas de automação na barra lateral, além de cartões de tarefas e lembretes nas sessões." + } +} diff --git a/packages/client/ui-schedule/locale/ru.json b/packages/client/ui-schedule/locale/ru.json new file mode 100644 index 0000000000..6abb8e9b65 --- /dev/null +++ b/packages/client/ui-schedule/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс задачи", + "description": "Страница задач автоматизации на боковой панели, а также карточки задач и отметки напоминаний в сеансах." + } +} diff --git a/packages/client/ui-schedule/src/client/DeleteToast.tsx b/packages/client/ui-schedule/src/client/DeleteToast.tsx index af3da62aff..751b44c7a0 100644 --- a/packages/client/ui-schedule/src/client/DeleteToast.tsx +++ b/packages/client/ui-schedule/src/client/DeleteToast.tsx @@ -1,23 +1,26 @@ /** * Transient outcome notice for schedule deletions. * - * Every deletion — the Tasks page detail, the session task tab, and the - * Session-header popover — reports its settled outcome into one store, and one - * `shell.overlay` entry renders it as the app-wide banner. The surfaces keep no - * in-place deletion notice of their own, so an outcome outlives the panel or - * tab that asked for it. + * Every deletion — the Tasks page detail, the session task tab, the + * Session-header popover, and the Tasks page's removal of every ended task — + * reports its settled outcome into one store, and one `shell.overlay` entry + * renders it as the app-wide banner. The surfaces keep no in-place deletion + * notice of their own, so an outcome outlives the panel or tab that asked for + * it. */ import type { ReactNode } from 'react' import { IconWarningOutlineRegular, Toast } from '@deepseek-ai/dsh-client-ui-primitives' import type { HostObservable, InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-layout/client' +import { assertNever } from '@deepseek-ai/dsh-util-values' import type { CatalogDeleteOutcome } from './catalog-source.ts' +import type { ClearEndedOutcome } from './clear-ended.ts' -/** One reported deletion outcome; `seq` keys the banner so a re-show restarts it. */ -export interface DeleteToastState { - readonly kind: 'deleted' | 'deleteFailed' - readonly seq: number -} +/** One reported outcome; `seq` keys the banner so a re-show restarts it. */ +export type DeleteToastState = + | { readonly kind: 'deleted' | 'deleteFailed'; readonly seq: number } + | { readonly kind: 'cleared'; readonly count: number; readonly seq: number } + | { readonly kind: 'clearFailed'; readonly seq: number } /** Store the deletion wiring reports into and the overlay entry reads. */ export interface DeleteToastSource { @@ -29,6 +32,11 @@ export interface DeleteToastSource { * @param outcome - resolution of `CatalogInjected.onDelete`. */ readonly report: (outcome: CatalogDeleteOutcome) => void + /** + * Show the notice of one settled bulk removal. + * @param outcome - resolution of the management page's bulk removal callback. + */ + readonly reportClear: (outcome: ClearEndedOutcome) => void /** Clear the shown notice. */ readonly dismiss: () => void } @@ -59,6 +67,11 @@ export function createDeleteToastSource(): DeleteToastSource { if (outcome === 'pending') return publish({ kind: outcome === 'deleted' ? 'deleted' : 'deleteFailed', seq: ++seq }) }, + reportClear: (outcome) => { + publish(outcome.cleared + ? { kind: 'cleared', count: outcome.count, seq: ++seq } + : { kind: 'clearFailed', seq: ++seq }) + }, dismiss: () => { publish(null) }, } } @@ -66,23 +79,40 @@ export function createDeleteToastSource(): DeleteToastSource { /** Props of the `shell.overlay` entry: the notice store and localized copy. */ export type ScheduleDeleteToastProps = PropsRuntime<'shell.overlay'> & PropsLocale<'schedule.manager'> - & InjectFace> + & InjectFace> /** - * Render the current deletion notice: a success banner for a confirmed - * deletion, a warning for one that could not be confirmed, or nothing. + * Render the current deletion notice: a success banner for a confirmed deletion + * or settled bulk removal, a warning for one that could not be confirmed, or + * nothing. * @param props - the notice hook, its dismissal, and the locale seat. * @returns the banner on display, or null. */ export function ScheduleDeleteToast({ useToast, dismiss, t }: ScheduleDeleteToastProps): ReactNode { const toast = useToast(current => current) if (toast === null) return null - return toast.kind === 'deleted' - ? - : } - onDone={dismiss} - /> + const key = `schedule-delete-${String(toast.seq)}` + switch (toast.kind) { + case 'deleted': + return + case 'deleteFailed': + return } onDone={dismiss} /> + case 'cleared': + // A removal that found nothing states that instead of a zero count, and + // without the success tone: the ended rows can leave between the + // confirmation and the request. + return toast.count === 0 + ? + : + case 'clearFailed': + return } onDone={dismiss} /> + /* v8 ignore next -- the store publishes only the union's members, so the default holds no reachable statement. */ + default: + return assertNever(toast) + } } diff --git a/packages/client/ui-schedule/src/client/TaskManagerPage.module.css b/packages/client/ui-schedule/src/client/TaskManagerPage.module.css index ac11fb33e0..7c4acf5a3d 100644 --- a/packages/client/ui-schedule/src/client/TaskManagerPage.module.css +++ b/packages/client/ui-schedule/src/client/TaskManagerPage.module.css @@ -87,8 +87,9 @@ white-space: nowrap; } -/* The Plugins toolbar's Add-plugin step: a 32px capsule with the 13px label. */ -.newButton { +/* The Plugins toolbar's steps: New and the bulk cleanup share one 32px + capsule with a 13px label. */ +.headerAction { height: 32px; font-size: 13px; line-height: 20px; @@ -148,11 +149,36 @@ neutral hairline, the 10px radius, and the mock's `--muted` leading icon. The mock brightens the border on hover and takes the brand stroke on focus rather than drawing a ring around the control. */ -.searchField { +.searchTools { + --task-search-height: 36px; display: flex; align-items: center; - height: 36px; - margin: 0 0 16px; + gap: 8px; + margin-bottom: 16px; +} + +.catalogActions { + flex: none; +} + +.catalogMore { + width: var(--task-search-height); + height: var(--task-search-height); + padding: 0; +} + +.catalogMore[aria-expanded='true'] { + background: var(--dsw-alias-interactive-bg-hover); +} + +.searchField { + box-sizing: border-box; + display: flex; + flex: 1; + min-width: 0; + align-items: center; + height: var(--task-search-height); + margin: 0; padding: 0 10px; border: 0.5px solid var(--dsw-alias-border-l3); border-radius: 12px; @@ -1465,6 +1491,9 @@ .deleteButton { color: var(--dsw-alias-state-error-primary); + /* The outlined confirmation button keeps its border in the danger colour so + the destructive action reads as a bordered control, not bare text. */ + --dsh-button-outline-color: var(--dsw-alias-state-error-primary); } .page :focus-visible { diff --git a/packages/client/ui-schedule/src/client/TaskManagerPage.tsx b/packages/client/ui-schedule/src/client/TaskManagerPage.tsx index 7d18063ae8..3c330c9f26 100644 --- a/packages/client/ui-schedule/src/client/TaskManagerPage.tsx +++ b/packages/client/ui-schedule/src/client/TaskManagerPage.tsx @@ -2,7 +2,8 @@ import { useEffect, useMemo, useRef, useState } from 'react' import clsx from 'clsx' import { - Button, IconClockOutlineRegular, IconCloseOutlineRegular, IconPlusOutlineRegular, IconSearchOutlineRegular, Input, + Button, IconClockOutlineRegular, IconCloseOutlineRegular, IconEllipsisOutlineRegular, IconPlusOutlineRegular, + IconSearchOutlineRegular, IconTrashOutlineRegular, Input, Menu, Modal, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives' import type { HostObservable, InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-layout/client' @@ -25,6 +26,16 @@ export interface TaskManagerInjected extends TaskDetailInjected { * schedule it. The page deliberately has no creation form of its own. */ readonly onNewTask: () => void + /** + * Remove every ended Host task, after this page has confirmed the action. + * + * The call requests no task ids, so the Host's ended set is the one removed; + * the page's rows are only a snapshot that a delivery can have moved. It + * resolves once the settled outcome has been reported through the app-wide + * notice, which is the only place the result is stated. + * @returns resolution after the outcome is reported. + */ + readonly onClearEnded: () => Promise } /** Root-scoped task catalog props derived from the framework and injected actions. */ @@ -40,12 +51,15 @@ type StatusFilter = 'all' | ScheduleCatalogEntry['status'] * @returns the searchable task list beside the selected task's detail. */ export function TaskManagerPage(props: TaskManagerPageProps) { - const { useCatalog, onNewTask, onRetry, t } = props + const { useCatalog, onNewTask, onClearEnded, onRetry, t } = props const catalog = useCatalog(snapshot => snapshot) const { records, status } = catalog const [search, setSearch] = useState('') - const [statusFilter, setStatusFilter] = useState('all') + const [statusFilter, setStatusFilter] = useState('active') const [selectedId, setSelectedId] = useState(null) + const [clearConfirm, setClearConfirm] = useState(false) + const [clearMenu, setClearMenu] = useState(false) + const [clearing, setClearing] = useState(false) // The rows state how long remains, so they read the shared ticking clock // rather than a value sampled at mount: a catalog refresh can move the target // this text describes. @@ -54,7 +68,20 @@ export function TaskManagerPage(props: TaskManagerPageProps) { const { record: catalogRecord, task: selected, id: detailId, confirmId, setConfirmId, setTab } = detail const rowRef = useRef(null) const headingRef = useRef(null) + const clearTriggerRef = useRef(null) + // Set when the confirmation opens: its settled removal can hide the trigger + // before the Modal returns focus, and so can another client's cleanup. + const clearFocusPending = useRef(false) + // Set when the removal settles: a readback superseded by `schedule/changed` + // can publish its rows after the dialog closed, removing the trigger then. + const clearSettled = useRef(false) const confirming = records.find(record => record.id === confirmId) + // The bulk action's target and count come from the complete catalog, not from + // the filtered rows: it removes every ended task, whatever the view shows. + const endedCount = useMemo( + () => records.filter(record => record.status === 'inactive').length, + [records], + ) const rows = useMemo(() => { const query = search.trim().toLowerCase() return records.filter(record => ( @@ -79,13 +106,15 @@ export function TaskManagerPage(props: TaskManagerPageProps) { useEffect(() => { // The detail focuses its own panel; with nothing selected, focus returns // to the row that opened it, or to the page heading once that row is gone. - if (selectedId !== null) return + // An open confirmation owns the focus until it closes, even when a refresh + // drops the selected task behind it. + if (selectedId !== null || clearConfirm) return if (rowRef.current !== null) { const target = rowRef.current.isConnected ? rowRef.current : headingRef.current target?.focus() rowRef.current = null } - }, [selectedId]) + }, [selectedId, clearConfirm]) useEffect(() => { if (status !== 'ready') return @@ -97,16 +126,55 @@ export function TaskManagerPage(props: TaskManagerPageProps) { setSelectedId(null) } + // The Modal restores focus to the element it captured when it opened; the + // trigger can be removed or rebuilt while the confirmation is open, so the + // page points focus at the current control, or the heading when it is gone. + // A settled removal whose readback lands later unmounts the trigger after the + // dialog closed, which drops focus to the document body; that handoff is + // caught by the same effect watching the ended count. + useEffect(() => { + if (clearConfirm) return + if (clearFocusPending.current) { + clearFocusPending.current = false + const trigger = clearTriggerRef.current + if (trigger?.isConnected === true) { + trigger.focus() + return + } + clearSettled.current = false + headingRef.current?.focus() + return + } + if (!clearSettled.current || endedCount > 0 || document.activeElement !== document.body) return + clearSettled.current = false + headingRef.current?.focus() + }, [clearConfirm, endedCount]) + + const confirmClearEnded = (): void => { + // The confirmation button stays focusable while the removal runs, so a + // second press has to be ignored here instead of by `disabled`. + if (clearing) return + setClearing(true) + void onClearEnded().finally(() => { + clearSettled.current = true + setClearing(false) + setClearConfirm(false) + }) + } + return (
{ - // A dropdown that closed on this Escape has already consumed the key: the - // Menu primitive closes from the document capture phase and calls - // preventDefault there, before React dispatches this handler. - if (event.key !== 'Escape' || event.defaultPrevented || confirmId !== null || selectedId === null) return + // An Escape that closed a dropdown is already consumed by the Menu's + // document capture listener. A confirmation's own Escape reaches the + // Modal's document bubble listener, which runs after this React handler, + // so the `clearConfirm` guard is what keeps this page from also closing + // the detail while the dialog closes. + if (event.key !== 'Escape' || event.defaultPrevented || confirmId !== null || clearConfirm + || selectedId === null) return event.preventDefault() event.stopPropagation() closeDetails() @@ -118,12 +186,12 @@ export function TaskManagerPage(props: TaskManagerPageProps) {

{t('title')}

- +
- {(['all', 'active', 'inactive'] as const).map(value => ( + {(['active', 'inactive', 'all'] as const).map(value => (
-
- } - aria-label={t('search.label')} - placeholder={t('search.placeholder')} - value={search} - onChange={(event) => { setSearch(event.target.value) }} - /> - {search !== '' && } +
+
+ } + aria-label={t('search.label')} + placeholder={t('search.placeholder')} + value={search} + onChange={(event) => { setSearch(event.target.value) }} + /> + {search !== '' && } +
+ {statusFilter !== 'active' && endedCount > 0 && { setClearMenu(false) }} + align="end" + portal + items={[{ + id: 'clear-inactive', label: t(clearing ? 'clear.pending' : 'clear.action'), + icon: , danger: true, + disabled: clearing || status !== 'ready', + }]} + onSelect={() => { + setClearMenu(false) + clearFocusPending.current = true + setClearConfirm(true) + }} + anchor={ + + } + />}
{selected === undefined && 0} onRetry={onRetry} t={t} />} @@ -209,6 +309,31 @@ export function TaskManagerPage(props: TaskManagerPageProps) { onDeleted={closeDetails} onClose={closeDetails} />} + { if (!clearing) setClearConfirm(false) }} + footer={
+ + +
} + /> +
) } diff --git a/packages/client/ui-schedule/src/client/clear-ended.ts b/packages/client/ui-schedule/src/client/clear-ended.ts new file mode 100644 index 0000000000..007d2b18d0 --- /dev/null +++ b/packages/client/ui-schedule/src/client/clear-ended.ts @@ -0,0 +1,45 @@ +/** Host-wide removal of every ended task, as the management page's one action reports it. */ +import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client' +import type { ScheduleDeleteInactiveResult } from '@deepseek-ai/dsh-schedule/client' + +/** How one bulk removal settled; the wiring reports it as an app-wide notice. */ +export type ClearEndedOutcome = + | { readonly cleared: true; readonly count: number } + | { readonly cleared: false } + +/** Remote removal and the authoritative readback that follows it. */ +export interface ClearEndedDependencies { + /** Ask the Host to remove every ended task; it answers how many rows it removed. */ + readonly removeInactive: () => Promise> + /** + * Read the catalog again, naming the newest request ordinal so a read sent + * before the removal is superseded rather than reused. + * @returns resolution after the read result is published. + */ + readonly refresh: () => Promise +} + +/** + * Create the management page's bulk removal callback. + * + * The call names no task ids: the Host owns which rows are ended, and a + * delivery can have moved the catalog between the confirmation and the + * request. The readback follows the acknowledged removal and supersedes a read + * issued before it, so the list follows the removal rather than a stale read. + * @param deps - Remote removal and its catalog readback. + * @returns a callback that resolves once the settled outcome is available. + */ +export function createClearEnded(deps: ClearEndedDependencies): () => Promise { + return async () => { + let result: RemoteResult + try { + result = await deps.removeInactive() + } catch (_error: unknown) { + // A rejected request does not establish which rows changed. + return { cleared: false } + } + if (!result.ok) return { cleared: false } + await deps.refresh() + return { cleared: true, count: result.value.deleted } + } +} diff --git a/packages/client/ui-schedule/src/client/frequency-locales.ts b/packages/client/ui-schedule/src/client/frequency-locales.ts index d09bd39d30..0c2ad0e963 100644 --- a/packages/client/ui-schedule/src/client/frequency-locales.ts +++ b/packages/client/ui-schedule/src/client/frequency-locales.ts @@ -183,3 +183,293 @@ export const frequencyJa = { 'relative.future': '{value}{unit}後', 'relative.overdue': '{value}{unit}超過', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const frequencyEs = { + 'time.locale': 'en', + 'time.utcPrefix': 'UTC', + 'frequency.daily': 'Diariamente en {time} ({timeZone})', + 'frequency.dailyLocal': 'Diariamente en {time}', + 'frequency.weekly': 'Semanalmente en {weekdays} en {time} ({timeZone})', + 'frequency.weeklyLocal': 'Semanalmente en {weekdays} en {time}', + 'frequency.cron': 'Cron {expression} ({timeZone})', + 'frequency.cronLocal': 'Cron {expression}', + 'frequency.cronRule': '{rule} ({timeZone})', + 'cron.list.join': ', ', + 'cron.part.join': ' ', + 'cron.weekday.name': '{weekday}', + 'cron.weekday.range': '{from}–{to}', + 'cron.months': ' en {months}', + 'cron.day.every': 'Todos los días{months}', + 'cron.day.weekdays': '{weekdays}{months}', + 'cron.day.monthDays': 'Día {days} de cada mes{months}', + 'cron.day.both': 'Día {days} de cada mes o {weekdays}{months}', + 'cron.day.bothStarred': 'Día {days} de cada mes y {weekdays}{months}', + 'cron.hours.range': '{from}–{to}', + 'cron.hours.list': '{hours}', + 'cron.time.everyMinute': 'cada minuto', + 'cron.time.everyMinutes': 'Cada minutos {step}', + 'cron.time.joinedEveryMinute': 'cada minuto', + 'cron.time.joinedEveryMinutes': 'cada {step} minutos', + 'cron.time.everyHour': 'cada hora', + 'cron.time.joinedEveryHour': 'cada hora', + 'cron.time.everyNHours': 'Cada horas {count}', + 'cron.time.joinedEveryNHours': 'cada horas {count}', + 'cron.time.hourlyAt': 'Cada hora al minuto {minutes}', + 'cron.time.joinedHourlyAt': 'cada hora al minuto {minutes}', + 'cron.time.hoursEveryMinute': 'cada minuto durante las horas {hours}', + 'cron.time.hoursEveryMinutes': 'cada {step} minutos durante las horas {hours}', + 'cron.time.at': 'en {times}', + 'cron.time.hoursAt': 'al minuto {minutes} de horas {hours}', + 'frequency.weekday.join': ', ', + 'frequency.weekday.1': 'lun', + 'frequency.weekday.2': 'mar', + 'frequency.weekday.3': 'mié', + 'frequency.weekday.4': 'jueves', + 'frequency.weekday.5': 'viernes', + 'frequency.weekday.6': 'sábado', + 'frequency.weekday.7': 'sol', + 'unit.day.one': 'dia', + 'unit.day.other': 'dias', + 'unit.hour.one': 'hora', + 'unit.hour.other': 'horas', + 'unit.minute.one': 'minuto', + 'unit.minute.other': 'minutos', + 'unit.second.one': 'segundo', + 'unit.second.other': 'segundos', + 'relative.now': 'Vencimiento ahora', + 'relative.future': 'en {value} {unit}', + 'relative.overdue': '{value} {unit} vencido', +} satisfies typeof frequencyEn + +/** French dictionary, checked against the English keys. */ +export const frequencyFr = { + 'time.locale': 'en', + 'time.utcPrefix': 'UTC', + 'frequency.daily': 'Tous les jours à {time} ({timeZone})', + 'frequency.dailyLocal': 'Tous les jours à {time}', + 'frequency.weekly': 'Hebdomadaire sur {weekdays} à {time} ({timeZone})', + 'frequency.weeklyLocal': 'Hebdomadaire sur {weekdays} à {time}', + 'frequency.cron': 'Cron {expression} ({timeZone})', + 'frequency.cronLocal': 'Cron {expression}', + 'frequency.cronRule': '{rule} ({timeZone})', + 'cron.list.join': ', ', + 'cron.part.join': ' ', + 'cron.weekday.name': '{weekday}', + 'cron.weekday.range': '{from}–{to}', + 'cron.months': ' dans {months}', + 'cron.day.every': 'Chaque jour{months}', + 'cron.day.weekdays': '{weekdays}{months}', + 'cron.day.monthDays': 'Jour {days} de chaque mois{months}', + 'cron.day.both': 'Jour {days} de chaque mois ou {weekdays}{months}', + 'cron.day.bothStarred': 'Jour {days} de chaque mois et {weekdays}{months}', + 'cron.hours.range': '{from}–{to}', + 'cron.hours.list': '{hours}', + 'cron.time.everyMinute': 'Chaque minute', + 'cron.time.everyMinutes': 'Toutes les minutes {step}', + 'cron.time.joinedEveryMinute': 'chaque minute', + 'cron.time.joinedEveryMinutes': 'toutes les minutes {step}', + 'cron.time.everyHour': 'Toutes les heures', + 'cron.time.joinedEveryHour': 'toutes les heures', + 'cron.time.everyNHours': 'Toutes les heures {count}', + 'cron.time.joinedEveryNHours': 'toutes les heures {count}', + 'cron.time.hourlyAt': 'Toutes les heures à la minute {minutes}', + 'cron.time.joinedHourlyAt': 'toutes les heures à la minute {minutes}', + 'cron.time.hoursEveryMinute': 'toutes les minutes pendant les heures {hours}', + 'cron.time.hoursEveryMinutes': 'toutes les minutes {step} pendant les heures {hours}', + 'cron.time.at': 'chez {times}', + 'cron.time.hoursAt': 'à la minute {minutes} des heures {hours}', + 'frequency.weekday.join': ', ', + 'frequency.weekday.1': 'lundi', + 'frequency.weekday.2': 'mar.', + 'frequency.weekday.3': 'mer', + 'frequency.weekday.4': 'jeu.', + 'frequency.weekday.5': 'vendredi', + 'frequency.weekday.6': 'Samedi', + 'frequency.weekday.7': 'Soleil', + 'unit.day.one': 'jour', + 'unit.day.other': 'jours', + 'unit.hour.one': 'heure', + 'unit.hour.other': 'heures', + 'unit.minute.one': 'minute', + 'unit.minute.other': 'minutes', + 'unit.second.one': 'seconde', + 'unit.second.other': 'secondes', + 'relative.now': 'À rendre maintenant', + 'relative.future': 'dans {value} {unit}', + 'relative.overdue': '{value} {unit} en retard', +} satisfies typeof frequencyEn + +/** German dictionary, checked against the English keys. */ +export const frequencyDe = { + 'time.locale': 'en', + 'time.utcPrefix': 'UTC', + 'frequency.daily': 'Täglich um {time} ({timeZone})', + 'frequency.dailyLocal': 'Täglich um {time}', + 'frequency.weekly': 'Wöchentlich auf {weekdays} um {time} ({timeZone})', + 'frequency.weeklyLocal': 'Wöchentlich auf {weekdays} und {time}', + 'frequency.cron': 'Cron {expression} ({timeZone})', + 'frequency.cronLocal': 'Cron {expression}', + 'frequency.cronRule': '{rule} ({timeZone})', + 'cron.list.join': ', ', + 'cron.part.join': ' ', + 'cron.weekday.name': '{weekday}', + 'cron.weekday.range': '{from}–{to}', + 'cron.months': ' in {months}', + 'cron.day.every': 'Jeden Tag{months}', + 'cron.day.weekdays': '{weekdays}{months}', + 'cron.day.monthDays': 'Tag {days} jedes Monats{months}', + 'cron.day.both': 'Tag {days} eines jeden Monats oder {weekdays}{months}', + 'cron.day.bothStarred': 'Tag {days} jedes Monats und {weekdays}{months}', + 'cron.hours.range': '{from}–{to}', + 'cron.hours.list': '{hours}', + 'cron.time.everyMinute': 'Jede Minute', + 'cron.time.everyMinutes': 'Alle {step} Minuten', + 'cron.time.joinedEveryMinute': 'jede Minute', + 'cron.time.joinedEveryMinutes': 'alle {step} Minuten', + 'cron.time.everyHour': 'Jede Stunde', + 'cron.time.joinedEveryHour': 'jede Stunde', + 'cron.time.everyNHours': 'Alle {count} Stunden', + 'cron.time.joinedEveryNHours': 'alle {count} Stunden', + 'cron.time.hourlyAt': 'Jede Stunde im Minutentakt {minutes}', + 'cron.time.joinedHourlyAt': 'jede Stunde im Minutentakt {minutes}', + 'cron.time.hoursEveryMinute': 'jede Minute während der Stunden {hours}', + 'cron.time.hoursEveryMinutes': 'alle {step} Minuten während der Stunden {hours}', + 'cron.time.at': 'bei {times}', + 'cron.time.hoursAt': 'zur Minute {minutes} der Stunden {hours}', + 'frequency.weekday.join': ', ', + 'frequency.weekday.1': 'Mo', + 'frequency.weekday.2': 'Di', + 'frequency.weekday.3': 'Mi', + 'frequency.weekday.4': 'Do', + 'frequency.weekday.5': 'Fr', + 'frequency.weekday.6': 'Sa', + 'frequency.weekday.7': 'Sonne', + 'unit.day.one': 'Tag', + 'unit.day.other': 'Tage', + 'unit.hour.one': 'Stunde', + 'unit.hour.other': 'Stunden', + 'unit.minute.one': 'Minute', + 'unit.minute.other': 'Minuten', + 'unit.second.one': 'zweitens', + 'unit.second.other': 'Sekunden', + 'relative.now': 'Jetzt fällig', + 'relative.future': 'in {value} {unit}', + 'relative.overdue': '{value} {unit} überfällig', +} satisfies typeof frequencyEn + +/** Russian dictionary, checked against the English keys. */ +export const frequencyRu = { + 'time.locale': 'en', + 'time.utcPrefix': 'UTC', + 'frequency.daily': 'Ежедневно по адресу {time} ({timeZone})', + 'frequency.dailyLocal': 'Ежедневно в {time}', + 'frequency.weekly': 'Еженедельно на {weekdays} по адресу {time} ({timeZone})', + 'frequency.weeklyLocal': 'Еженедельно на {weekdays} по адресу {time}', + 'frequency.cron': 'Крон {expression} ({timeZone})', + 'frequency.cronLocal': 'Крон {expression}', + 'frequency.cronRule': '{rule} ({timeZone})', + 'cron.list.join': ', ', + 'cron.part.join': ' ', + 'cron.weekday.name': '{weekday}', + 'cron.weekday.range': '{from} – {to}', + 'cron.months': ' в {months}', + 'cron.day.every': 'Каждый день{months}', + 'cron.day.weekdays': '{weekdays}{months}', + 'cron.day.monthDays': 'День {days} каждого месяца{months}', + 'cron.day.both': 'День {days} каждого месяца или {weekdays}{months}.', + 'cron.day.bothStarred': 'День {days} каждого месяца и {weekdays}{months}.', + 'cron.hours.range': '{from} – {to}', + 'cron.hours.list': '{hours}', + 'cron.time.everyMinute': 'Каждую минуту', + 'cron.time.everyMinutes': 'Каждые {step} минут', + 'cron.time.joinedEveryMinute': 'каждую минуту', + 'cron.time.joinedEveryMinutes': 'каждые {step} минут', + 'cron.time.everyHour': 'Каждый час', + 'cron.time.joinedEveryHour': 'каждый час', + 'cron.time.everyNHours': 'Каждые {count} часов', + 'cron.time.joinedEveryNHours': 'каждые {count} часов', + 'cron.time.hourlyAt': 'Каждый час в минуту {minutes}', + 'cron.time.joinedHourlyAt': 'каждый час в минуту {minutes}', + 'cron.time.hoursEveryMinute': 'каждую минуту в течение часов {hours}', + 'cron.time.hoursEveryMinutes': 'каждые {step} минуты в течение часов {hours}', + 'cron.time.at': 'в {times}', + 'cron.time.hoursAt': 'в минуту {minutes} часов {hours}', + 'frequency.weekday.join': ', ', + 'frequency.weekday.1': 'Пн.', + 'frequency.weekday.2': 'Вт', + 'frequency.weekday.3': 'ср.', + 'frequency.weekday.4': 'Чт', + 'frequency.weekday.5': 'Пт', + 'frequency.weekday.6': 'Суббота', + 'frequency.weekday.7': 'Солнце', + 'unit.day.one': 'день', + 'unit.day.other': 'дни', + 'unit.hour.one': 'час', + 'unit.hour.other': 'часы', + 'unit.minute.one': 'минута', + 'unit.minute.other': 'минуты', + 'unit.second.one': 'второй', + 'unit.second.other': 'секунды', + 'relative.now': 'Срок оплаты сейчас', + 'relative.future': 'в {value} {unit}', + 'relative.overdue': '{value} {unit} просрочен', +} satisfies typeof frequencyEn + +/** Portuguese dictionary, checked against the English keys. */ +export const frequencyPt = { + 'time.locale': 'en', + 'time.utcPrefix': 'UTC', + 'frequency.daily': 'Diariamente às {time} ({timeZone})', + 'frequency.dailyLocal': 'Diariamente às {time}', + 'frequency.weekly': 'Semanalmente em {weekdays} às {time} ({timeZone})', + 'frequency.weeklyLocal': 'Semanalmente em {weekdays} às {time}', + 'frequency.cron': 'Cron {expression} ({timeZone})', + 'frequency.cronLocal': 'Cron {expression}', + 'frequency.cronRule': '{rule} ({timeZone})', + 'cron.list.join': ', ', + 'cron.part.join': ' ', + 'cron.weekday.name': '{weekday}', + 'cron.weekday.range': '{from}–{to}', + 'cron.months': ' em {months}', + 'cron.day.every': 'Todos os dias{months}', + 'cron.day.weekdays': '{weekdays}{months}', + 'cron.day.monthDays': 'Dia {days} de cada mês{months}', + 'cron.day.both': 'Dia {days} de cada mês ou {weekdays}{months}', + 'cron.day.bothStarred': 'Dia {days} de cada mês e {weekdays}{months}', + 'cron.hours.range': '{from}–{to}', + 'cron.hours.list': '{hours}', + 'cron.time.everyMinute': 'Cada minuto', + 'cron.time.everyMinutes': 'A cada {step} minutos', + 'cron.time.joinedEveryMinute': 'cada minuto', + 'cron.time.joinedEveryMinutes': 'a cada {step} minutos', + 'cron.time.everyHour': 'A cada hora', + 'cron.time.joinedEveryHour': 'a cada hora', + 'cron.time.everyNHours': 'A cada {count} horas', + 'cron.time.joinedEveryNHours': 'a cada {count} horas', + 'cron.time.hourlyAt': 'A cada hora, minuto {minutes}', + 'cron.time.joinedHourlyAt': 'a cada hora por minuto {minutes}', + 'cron.time.hoursEveryMinute': 'a cada minuto durante horas {hours}', + 'cron.time.hoursEveryMinutes': 'a cada {step} minutos durante horas {hours}', + 'cron.time.at': 'em {times}', + 'cron.time.hoursAt': 'no minuto {minutes} das horas {hours}', + 'frequency.weekday.join': ', ', + 'frequency.weekday.1': 'segunda-feira', + 'frequency.weekday.2': 'Terça', + 'frequency.weekday.3': 'Quarta', + 'frequency.weekday.4': 'Qui', + 'frequency.weekday.5': 'Sexta-feira', + 'frequency.weekday.6': 'Sábado', + 'frequency.weekday.7': 'Sol', + 'unit.day.one': 'dia', + 'unit.day.other': 'dias', + 'unit.hour.one': 'hora', + 'unit.hour.other': 'horas', + 'unit.minute.one': 'minuto', + 'unit.minute.other': 'minutos', + 'unit.second.one': 'segundo', + 'unit.second.other': 'segundos', + 'relative.now': 'Vencimento agora', + 'relative.future': 'em {value} {unit}', + 'relative.overdue': '{value} {unit} atrasado', +} satisfies typeof frequencyEn diff --git a/packages/client/ui-schedule/src/client/index.ts b/packages/client/ui-schedule/src/client/index.ts index 8c6dcb3c5f..7dcbc32ee3 100644 --- a/packages/client/ui-schedule/src/client/index.ts +++ b/packages/client/ui-schedule/src/client/index.ts @@ -44,6 +44,7 @@ import type {} from '@deepseek-ai/dsh-client-connection/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' import type { ScheduleCatalogEntry, ScheduleDeliveryHistoryRequest, ScheduleId, ScheduleUpdateRequest } from '@deepseek-ai/dsh-schedule/client' import { createCatalogSource, type CatalogDeleteOutcome, type CatalogInjected } from './catalog-source.ts' +import { createClearEnded } from './clear-ended.ts' import { createDeleteToastSource, ScheduleDeleteToast } from './DeleteToast.tsx' import { SCHEDULE_TASK_ID, SCHEDULE_TASK_KIND, scheduleTaskDefinition } from './definition.ts' import { ScheduleCatalogAction } from './ScheduleCatalogAction.tsx' @@ -59,8 +60,8 @@ import { TaskManagerPage, type TaskManagerInjected } from './TaskManagerPage.tsx import type { TaskDetailInjected } from './TaskDetail.tsx' import { TaskManagerIcon } from './TaskManagerIcon.tsx' import { sessionLinkState } from './session-link.ts' -import { en, ja, NS, zh, type ScheduleCatalogKey } from './locales.ts' -import { en as managerEn, ja as managerJa, zh as managerZh, type TaskManagerKey } from './task-manager-locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type ScheduleCatalogKey } from './locales.ts' +import { en as managerEn, ja as managerJa, es as managerEs, fr as managerFr, de as managerDe, ru as managerRu, pt as managerPt, zh as managerZh, type TaskManagerKey } from './task-manager-locales.ts' const MANAGER_NS = 'schedule.manager' const PANEL_ID = 'schedules' as MainPanelId @@ -87,8 +88,17 @@ export const inject = [ * @param ctx - browser services used by these contributions. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-schedule: dictionaries') - const managerDictionaries = { zh: managerZh, en: managerEn, ja: managerJa } + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-schedule: dictionaries') + const managerDictionaries = { + zh: managerZh, + en: managerEn, + ja: managerJa, + es: managerEs, + fr: managerFr, + de: managerDe, + ru: managerRu, + pt: managerPt, + } ctx.effect(() => ctx.locale.register(MANAGER_NS, managerDictionaries), 'ui-schedule: manager dictionaries') const t = ctx.locale.bind(MANAGER_NS) ctx.effect(() => ctx.sidebarRightTabs.register(scheduleTaskDefinition(t)), 'ui-schedule: task tab type') @@ -145,6 +155,17 @@ export function apply(ctx: ClientContext): void { ctx.uiWorkspace.openSession(id) } } + /** + * Remove every ended Host task and read the catalog back. + * + * The readback names every request seen so far, so it supersedes a read sent + * before the removal instead of joining it. + * @returns the settled bulk-removal outcome. + */ + const clearEnded = createClearEnded({ + removeInactive: () => ctx.remote.schedule.deleteInactive(), + refresh: () => manager.onRetry(manager.hooks.catalog.getSnapshot().readRequest), + }) const detail: TaskDetailInjected & ScheduleTaskCatalogInjected = { hooks: manager.hooks, onDelete: reportedDelete(manager.onDelete), @@ -180,6 +201,10 @@ export function apply(ctx: ClientContext): void { ...detail, // The page has no creation form: a new reminder starts in a Session. onNewTask: () => { ctx.uiWorkspace.startSession() }, + onClearEnded: async () => { + const outcome = await clearEnded() + deleteToast.reportClear(outcome) + }, }), }, TaskManagerPage)) yield ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({ diff --git a/packages/client/ui-schedule/src/client/locales.ts b/packages/client/ui-schedule/src/client/locales.ts index ff04c31ce4..1ded1fa0c6 100644 --- a/packages/client/ui-schedule/src/client/locales.ts +++ b/packages/client/ui-schedule/src/client/locales.ts @@ -2,7 +2,7 @@ * `schedule.catalog` namespace dictionaries: the Session header catalog and the * Sidebar row mark with its hover-card task section. */ -import { frequencyEn, frequencyJa, frequencyZh } from './frequency-locales.ts' +import { frequencyEn, frequencyJa, frequencyEs, frequencyFr, frequencyDe, frequencyRu, frequencyPt, frequencyZh } from './frequency-locales.ts' /** Dictionary namespace owned by this plugin. */ export const NS = 'schedule.catalog' @@ -72,3 +72,108 @@ export const ja = { 'mark.aria': 'スケジュールされたタスク {count}件', 'hover.more': '他 {count}件', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'trigger.label': 'Recordatorios', + 'list.loading': 'Cargando recordatorios…', + 'list.error': 'No se pudieron cargar los recordatorios.', + 'list.retry': 'Reintentar', + 'delete.action': 'Eliminar', + 'delete.pending': 'Eliminando…', + 'delete.label': 'Eliminar recordatorio: {title}', + 'list.open': 'Abrir detalles del recordatorio: {title}', + 'trigger.one': 'Recordatorio de {count}', + 'trigger.other': 'Recordatorios de {count}', + 'list.aria': 'Recordatorios activos', + 'list.nextRun': 'Próxima ejecución', + 'frequency.once': 'una vez', + 'frequency.every': 'Cada {value} {unit}', + ...frequencyEs, + 'mark.aria': 'Tareas programadas {count}', + 'hover.more': '{count} más', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'trigger.label': 'Rappels', + 'list.loading': 'Chargement des rappels…', + 'list.error': 'Impossible de charger les rappels.', + 'list.retry': 'Réessayer', + 'delete.action': 'Supprimer', + 'delete.pending': 'Suppression…', + 'delete.label': 'Supprimer le rappel : {title}', + 'list.open': 'Détails du rappel ouvert : {title}', + 'trigger.one': 'Rappel {count}', + 'trigger.other': 'Rappels {count}', + 'list.aria': 'Rappels actifs', + 'list.nextRun': 'Prochaine exécution', + 'frequency.once': 'Une fois', + 'frequency.every': 'Chaque {value} {unit}', + ...frequencyFr, + 'mark.aria': 'Tâches planifiées {count}', + 'hover.more': '{count} plus', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'trigger.label': 'Erinnerungen', + 'list.loading': 'Erinnerungen werden geladen…', + 'list.error': 'Erinnerungen konnten nicht geladen werden.', + 'list.retry': 'Erneut versuchen', + 'delete.action': 'Löschen', + 'delete.pending': 'Löschen…', + 'delete.label': 'Erinnerung löschen: {title}', + 'list.open': 'Offene Erinnerungsdetails: {title}', + 'trigger.one': '{count} Erinnerung', + 'trigger.other': '{count} Erinnerungen', + 'list.aria': 'Aktive Erinnerungen', + 'list.nextRun': 'Nächster Lauf', + 'frequency.once': 'Einmal', + 'frequency.every': 'Jeder {value} {unit}', + ...frequencyDe, + 'mark.aria': '{count} geplante Aufgaben', + 'hover.more': '{count} mehr', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'trigger.label': 'Напоминания', + 'list.loading': 'Загрузка напоминаний…', + 'list.error': 'Не удалось загрузить напоминания.', + 'list.retry': 'Повторить', + 'delete.action': 'Удалить', + 'delete.pending': 'Удаление…', + 'delete.label': 'Удалить напоминание: {title}', + 'list.open': 'Открыть детали напоминания: {title}.', + 'trigger.one': '{count} напоминание', + 'trigger.other': '{count} напоминания', + 'list.aria': 'Активные напоминания', + 'list.nextRun': 'Следующий запуск', + 'frequency.once': 'Однажды', + 'frequency.every': 'Каждый {value} {unit}', + ...frequencyRu, + 'mark.aria': '{count} запланированные задачи', + 'hover.more': '{count} подробнее', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'trigger.label': 'Lembretes', + 'list.loading': 'Carregando lembretes…', + 'list.error': 'Não foi possível carregar os lembretes.', + 'list.retry': 'Tentar novamente', + 'delete.action': 'Excluir', + 'delete.pending': 'Excluindo…', + 'delete.label': 'Excluir lembrete: {title}', + 'list.open': 'Detalhes do lembrete aberto: {title}', + 'trigger.one': '{count} lembrete', + 'trigger.other': '{count} lembretes', + 'list.aria': 'Lembretes ativos', + 'list.nextRun': 'Próxima execução', + 'frequency.once': 'Uma vez', + 'frequency.every': 'A cada {value} {unit}', + ...frequencyPt, + 'mark.aria': '{count} tarefas agendadas', + 'hover.more': '{count} mais', +} satisfies Record diff --git a/packages/client/ui-schedule/src/client/task-manager-locales.ts b/packages/client/ui-schedule/src/client/task-manager-locales.ts index 7917d8dbb5..1b5f513f74 100644 --- a/packages/client/ui-schedule/src/client/task-manager-locales.ts +++ b/packages/client/ui-schedule/src/client/task-manager-locales.ts @@ -1,5 +1,5 @@ /** Copy for the retained-reminder task manager's `schedule.manager` namespace. */ -import { frequencyEn, frequencyJa, frequencyZh } from './frequency-locales.ts' +import { frequencyEn, frequencyJa, frequencyEs, frequencyFr, frequencyDe, frequencyRu, frequencyPt, frequencyZh } from './frequency-locales.ts' /** English task-manager dictionary and key domain. */ export const en = { @@ -134,8 +134,20 @@ export const en = { 'delete.cancel': 'Cancel', 'delete.close': 'Close deletion confirmation', 'delete.pending': 'Deleting…', + 'list.more': 'More task actions', + 'clear.action': 'Clear all inactive tasks', + 'clear.title': 'Clear {count} inactive tasks?', + 'clear.description': 'These tasks and their saved delivery records will be permanently deleted. ' + + 'Enabled tasks, original sessions, and queued messages are not affected.', + 'clear.confirm': 'Clear tasks', + 'clear.pending': 'Clearing…', + 'clear.close': 'Close clear confirmation', 'toast.deleted': 'Task deleted.', 'toast.deleteFailed': 'Could not delete the task.', + 'toast.cleared.one': 'Cleared {count} inactive task.', + 'toast.cleared.other': 'Cleared {count} inactive tasks.', + 'toast.clearedNone': 'No inactive tasks to clear.', + 'toast.clearFailed': 'Could not clear tasks.', 'card.open': 'Open', 'card.openLabel': 'Open task details: {title}', 'card.deleted': 'Deleted', @@ -276,8 +288,19 @@ export const zh: Record = { 'delete.cancel': '取消', 'delete.close': '关闭删除确认', 'delete.pending': '正在删除…', + 'list.more': '更多任务操作', + 'clear.action': '清除全部已结束任务', + 'clear.title': '清除 {count} 个已结束任务?', + 'clear.description': '这些任务及其运行记录将被永久删除,已开启任务、原会话和已排队消息等不受影响。', + 'clear.confirm': '确认清除', + 'clear.pending': '正在清理…', + 'clear.close': '关闭清理确认', 'toast.deleted': '任务已删除', 'toast.deleteFailed': '无法删除任务', + 'toast.cleared.one': '已清除 {count} 个已结束任务', + 'toast.cleared.other': '已清除 {count} 个已结束任务', + 'toast.clearedNone': '没有可清理的已结束任务', + 'toast.clearFailed': '清除任务失败', 'card.open': '打开', 'card.openLabel': '打开任务详情:{title}', 'card.deleted': '已删除', @@ -415,10 +438,726 @@ export const ja = { 'delete.cancel': 'キャンセル', 'delete.close': '削除確認を閉じる', 'delete.pending': '削除中…', + 'list.more': 'その他のタスク操作', + 'clear.action': '終了済みタスクをすべてクリア', + 'clear.title': '終了済みタスク {count} 件をクリアしますか?', + 'clear.description': 'これらのタスクと実行履歴は完全に削除されます。実行中のタスク、元のセッション、キューに入ったメッセージなどは影響を受けません。', + 'clear.confirm': 'クリアする', + 'clear.pending': 'クリア中…', + 'clear.close': 'クリア確認を閉じる', 'toast.deleted': 'タスクを削除しました。', 'toast.deleteFailed': 'タスクを削除できませんでした。', + 'toast.cleared.one': '終了したタスクを {count} 件クリアしました。', + 'toast.cleared.other': '終了したタスクを {count} 件クリアしました。', + 'toast.clearedNone': 'クリアする終了済みタスクはありません。', + 'toast.clearFailed': 'タスクをクリアできませんでした。', 'card.open': '開く', 'card.openLabel': 'タスクの詳細を開く: {title}', 'card.deleted': '削除済み', 'tool.invoked': '{name} を実行しました', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'panel': 'Tareas de automatización', + 'title': 'Tareas de automatización', + 'new.action': 'Nuevo', + 'list.label': 'Catálogo de tareas', + 'list.nextPrefix': 'Próxima hora prevista: ', + 'list.loading': 'Cargando tareas…', + 'list.error': 'No se pudieron cargar las tareas.', + 'list.retry': 'Reintentar', + 'list.empty': 'Aún no hay tareas. Las tareas creadas en tus sesiones aparecen aquí.', + 'list.emptyInactive': 'Sin tareas de automatización inactivas', + 'list.noMatches': 'No hay tareas coincidentes', + 'search.label': 'Tareas de búsqueda', + 'search.placeholder': 'Tareas de automatización de búsqueda', + 'search.clear': 'Borrar búsqueda', + 'empty.action': 'Nueva tarea de automatización', + 'statusFilter.label': 'Estado de la tarea', + 'statusFilter.all': 'Todos', + 'status.active': 'Habilitado', + 'status.inactive': 'Inactivo', + 'detail.label': 'Detalles de la tarea', + 'detail.close': 'Cerrar detalles', + 'detail.more': 'Acciones de tareas de automatización', + 'detail.nextRun': 'Próxima ejecución', + 'detail.tabs': 'Vistas de detalles de tareas', + 'detail.rule': 'Reglas', + 'detail.records': 'Registros', + 'delivery.empty': 'No hay registro de entrega disponible', + 'delivery.loading': 'Cargando registros de entrega…', + 'delivery.error': 'No se pudieron cargar los registros de entrega.', + 'delivery.notFound': 'Esta tarea ya no está disponible.', + 'delivery.cursorError': 'Se actualizaron los registros.', + 'delivery.retry': 'Reintentar', + 'delivery.refresh': 'Actualizar', + 'delivery.loadMore': 'Cargar más', + 'delivery.expand': 'Expandir', + 'delivery.collapse': 'Contraer', + 'delivery.pruned': 'Se han borrado los registros de entrega anteriores.', + 'delivery.retention': 'Reglas de retención', + 'delivery.retentionBounds': 'Cada tarea mantiene registros de entrega hasta {records} de los últimos días {days}.', + 'delivery.retentionExplanation': 'Cuando se guarda un nuevo registro, los registros más antiguos que estén fuera de estos límites se borran automáticamente. Borrar registros no detiene la tarea.', + 'detail.name': 'Nombre de la tarea de automatización', + 'detail.instruction': 'Instrucción de tarea de automatización', + 'detail.status': 'Estado', + 'detail.next': 'Próxima hora programada', + 'detail.frequency': 'Frecuencia', + 'detail.id': 'ID de tarea', + 'detail.session': 'Sesión vinculada', + 'detail.openSession': 'Sesión vinculada: abrir sesión original', + 'detail.openSessionTitle': 'Sesión vinculada {title}: abrir sesión original', + 'detail.sessionLoading': 'Cargando información de la sesión original.', + 'detail.sessionArchived': 'La sesión original está archivada.', + 'detail.sessionUnavailable': 'La sesión original no está disponible.', + 'detail.missing': 'Tarea no disponible. Es posible que haya sido eliminado.', + 'delivery.label': 'Registro de entrega guardado', + 'frequency.once': 'una vez', + 'frequency.every': 'Cada {value} {unit}', + ...frequencyEs, + 'timing.date': 'Fecha', + 'timing.time': 'tiempo', + 'timing.hour': 'Hora', + 'timing.minute': 'Minuto', + 'timing.second': 'segundo', + 'timing.prevMonth': 'Mes anterior', + 'timing.nextMonth': 'El próximo mes', + 'timing.zone': 'Zona horaria', + 'timing.zoneSearch': 'Buscar por desplazamiento UTC, ID de IANA o ciudad', + 'timing.zoneNoResults': 'No hay zonas horarias coincidentes', + 'timing.interval': 'Repetir cada', + 'timing.intervalIncrease': 'aumentar el intervalo', + 'timing.intervalDecrease': 'Intervalo de disminución', + 'timing.unit.hour': 'horas', + 'timing.unit.minute': 'minutos', + 'timing.unit.second': 'segundos', + 'timing.intervalHint.hour': 'Al menos 1 hora. El intervalo cuenta desde la creación de la tarea o el último cambio de regla y es independiente de las zonas horarias.', + 'timing.intervalHint.minute': 'Al menos 1 minuto. El intervalo cuenta desde la creación de la tarea o el último cambio de regla y es independiente de las zonas horarias.', + 'timing.intervalHint.second': 'Al menos 60 segundos. El intervalo cuenta desde la creación de la tarea o el último cambio de regla y es independiente de las zonas horarias.', + 'timing.zoneNoStored': 'Una tarea única almacena sólo su momento objetivo, no una zona horaria. La fecha y la hora se interpretan en la zona horaria seleccionada.', + 'timing.inactive': 'Las tareas inactivas son de solo lectura. Su sincronización no se puede editar.', + 'timing.conflict': 'Esta tarea cambió mientras estabas editando. Su borrador se conserva. ' + + 'Cancele y vuelva a abrir después de actualizar para editar la regla más reciente.', + 'timing.notFound': 'Esta tarea ya no está disponible. Su borrador se conserva; cancelar para cerrar el editor.', + 'timing.subagentSession': 'Esta sesión pertenece a un subagente, que nunca recibe recordatorios entregados.', + 'timing.invalid': 'Ingrese una fecha y hora válidas, o verifique los campos de tiempo.', + 'timing.invalidZone': 'Ingrese una zona horaria IANA válida, por ejemplo Asia/Shanghai.', + 'timing.notFuture': 'Elija una fecha y hora en el futuro.', + 'timing.invalidInterval': 'Introduzca un intervalo de al menos 1 minuto.', + 'timing.invalidInterval.hour': 'Introduzca un intervalo de al menos 1 hora.', + 'timing.invalidInterval.minute': 'Introduzca un intervalo de al menos 1 minuto.', + 'timing.invalidInterval.second': 'Introduzca un intervalo de al menos 60 segundos.', + 'timing.error': 'No se pudo confirmar la actualización del tiempo. Su borrador se conserva. Verifique la tarea antes de volver a intentarlo.', + 'rule.title': 'tiempo de ejecución', + 'rule.repeat': 'repetir', + 'rule.weekday': 'Día laborable', + 'rule.weekdayOption': 'Día laborable {weekday}', + 'rule.unsaved': 'Cambios no guardados', + 'rule.save': 'Guardar cambios', + 'rule.saving': 'Guardando…', + 'rule.cancel': 'Cancelar', + 'rule.invalidTitle': 'Introduzca un nombre de tarea de como máximo 120 caracteres.', + 'rule.invalidPrompt': 'Introduzca una instrucción de tarea.', + 'rule.once': 'una vez', + 'rule.everyMinutes': 'Cada N minutos', + 'rule.everyHours': 'Cada N horas', + 'rule.everySeconds': 'Cada N segundos', + 'rule.daily': 'todos los dias', + 'rule.weekdays': 'lunes a viernes', + 'rule.weekly': 'Semanal', + 'rule.cron': 'personalizado', + 'rule.cronLabel': 'expresión cron', + 'rule.cronInvalid': 'Ingrese una expresión cron de cinco campos, por ejemplo 0 9 * * 1-5.', + 'cronForm.frequency': 'Frecuencia', + 'cronForm.monthly': 'Mensual', + 'cronForm.weekly': 'Semanal', + 'cronForm.daily': 'todos los dias', + 'cronForm.dates': 'en fechas', + 'cronForm.dateOption': 'Día {day}', + 'cronForm.atMinute': 'al minuto', + 'cronForm.minuteIncrease': 'aumentar minuto', + 'cronForm.minuteDecrease': 'Disminuir minuto', + 'rule.zone.system': ' (sistema)', + 'rule.error.conflict': 'Esta tarea cambió antes de que se guardara la actualización. La fila muestra la regla guardada; intentar otra vez.', + 'rule.error.notFound': 'Esta tarea ya no está disponible.', + 'rule.error.unknown': 'No se pudo confirmar la actualización de la regla. La fila muestra la regla guardada.', + 'delete.action': 'Eliminar tarea', + 'delete.title': '¿Eliminar esta tarea?', + 'delete.description': 'Esto evita que la tarea se active y la elimina junto con sus registros de entrega guardados. ' + + 'La sesión original y sus mensajes permanecen; los mensajes en cola no se retiran.', + 'delete.confirm': 'Confirmar eliminación', + 'delete.cancel': 'Cancelar', + 'delete.close': 'Cerrar confirmación de eliminación', + 'delete.pending': 'Eliminando…', + 'toast.deleted': 'Tarea eliminada.', + 'toast.deleteFailed': 'No se pudo eliminar la tarea.', + 'card.open': 'Abrir', + 'card.openLabel': 'Abrir detalles de la tarea: {title}', + 'card.deleted': 'Eliminado', + 'tool.invoked': 'Invocado {name}', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'panel': "Tâches d'automatisation", + 'title': "Tâches d'automatisation", + 'new.action': 'Nouveau', + 'list.label': 'Catalogue de tâches', + 'list.nextPrefix': 'Prochaine heure programmée : ', + 'list.loading': 'Chargement des tâches…', + 'list.error': 'Impossible de charger les tâches.', + 'list.retry': 'Réessayer', + 'list.empty': "Aucune tâche pour l'instant. Les tâches créées dans vos sessions apparaissent ici.", + 'list.emptyInactive': "Aucune tâche d'automatisation inactive", + 'list.noMatches': 'Aucune tâche correspondante', + 'search.label': 'Tâches de recherche', + 'search.placeholder': "Tâches d'automatisation de la recherche", + 'search.clear': 'Effacer la recherche', + 'empty.action': "Nouvelle tâche d'automatisation", + 'statusFilter.label': 'Statut de la tâche', + 'statusFilter.all': 'Tout', + 'status.active': 'Activé', + 'status.inactive': 'Inactif', + 'detail.label': 'Détails de la tâche', + 'detail.close': 'Fermer les détails', + 'detail.more': "Actions des tâches d'automatisation", + 'detail.nextRun': 'Prochaine exécution', + 'detail.tabs': 'Vues détaillées des tâches', + 'detail.rule': 'Règles', + 'detail.records': 'Enregistrements', + 'delivery.empty': 'Aucun historique de livraison disponible', + 'delivery.loading': 'Chargement des enregistrements de livraison…', + 'delivery.error': 'Impossible de charger les enregistrements de livraison.', + 'delivery.notFound': "Cette tâche n'est plus disponible.", + 'delivery.cursorError': 'Les dossiers ont été mis à jour.', + 'delivery.retry': 'Réessayer', + 'delivery.refresh': 'Actualiser', + 'delivery.loadMore': 'Charger plus', + 'delivery.expand': 'Développer', + 'delivery.collapse': 'Réduire', + 'delivery.pruned': 'Les enregistrements de livraison antérieurs ont été effacés', + 'delivery.retention': 'Règles de conservation', + 'delivery.retentionBounds': "Chaque tâche conserve jusqu'à {records} enregistrements de livraison des derniers jours {days}.", + 'delivery.retentionExplanation': "Lorsqu'un nouvel enregistrement est enregistré, les enregistrements plus anciens en dehors de ces limites sont automatiquement effacés. La suppression des enregistrements n'arrête pas la tâche.", + 'detail.name': "Nom de la tâche d'automatisation", + 'detail.instruction': "Instruction de tâche d'automatisation", + 'detail.status': 'Statut', + 'detail.next': 'Prochaine heure programmée', + 'detail.frequency': 'Fréquence', + 'detail.id': 'ID de tâche', + 'detail.session': 'Séance liée', + 'detail.openSession': 'Session liée : ouvrir la session originale', + 'detail.openSessionTitle': 'Session liée {title} : ouvrir la session originale', + 'detail.sessionLoading': 'Chargement des informations de session originales.', + 'detail.sessionArchived': 'La session originale est archivée.', + 'detail.sessionUnavailable': "La session originale n'est pas disponible.", + 'detail.missing': 'Tâche indisponible. Il a peut-être été supprimé.', + 'delivery.label': 'Dossier de livraison enregistré', + 'frequency.once': 'Une fois', + 'frequency.every': 'Chaque {value} {unit}', + ...frequencyFr, + 'timing.date': 'Date', + 'timing.time': 'Temps', + 'timing.hour': 'Heure', + 'timing.minute': 'Minutes', + 'timing.second': 'Seconde', + 'timing.prevMonth': 'Mois précédent', + 'timing.nextMonth': 'Le mois prochain', + 'timing.zone': 'Fuseau horaire', + 'timing.zoneSearch': 'Recherche par décalage UTC, ID IANA ou ville', + 'timing.zoneNoResults': 'Aucun fuseau horaire correspondant', + 'timing.interval': 'Répétez tous les', + 'timing.intervalIncrease': "Augmenter l'intervalle", + 'timing.intervalDecrease': "Diminuer l'intervalle", + 'timing.unit.hour': 'heures', + 'timing.unit.minute': 'minutes', + 'timing.unit.second': 'secondes', + 'timing.intervalHint.hour': "Au moins 1 heure. L'intervalle compte à partir de la création de la tâche ou du dernier changement de règle et est indépendant des fuseaux horaires.", + 'timing.intervalHint.minute': "Au moins 1 minute. L'intervalle compte à partir de la création de la tâche ou du dernier changement de règle et est indépendant des fuseaux horaires.", + 'timing.intervalHint.second': "Au moins 60 secondes. L'intervalle compte à partir de la création de la tâche ou du dernier changement de règle et est indépendant des fuseaux horaires.", + 'timing.zoneNoStored': "Une tâche ponctuelle stocke uniquement son moment cible, pas un fuseau horaire. La date et l'heure sont interprétées dans le fuseau horaire sélectionné.", + 'timing.inactive': 'Les tâches inactives sont en lecture seule. Leur timing ne peut pas être modifié.', + 'timing.conflict': 'Cette tâche a changé pendant que vous effectuiez la modification. Votre brouillon est conservé. ' + + "Annulez et rouvrez après l'actualisation pour modifier la dernière règle.", + 'timing.notFound': "Cette tâche n'est plus disponible. Votre brouillon est conservé ; annuler pour fermer l'éditeur.", + 'timing.subagentSession': 'Cette session appartient à un sous-agent, qui ne reçoit jamais de rappels délivrés.', + 'timing.invalid': 'Entrez une date et une heure valides ou vérifiez les champs de synchronisation.', + 'timing.invalidZone': 'Saisissez un fuseau horaire IANA valide, par exemple Asie/Shanghai.', + 'timing.notFuture': 'Choisissez une date et une heure dans le futur.', + 'timing.invalidInterval': "Entrez un intervalle d'au moins 1 minute.", + 'timing.invalidInterval.hour': "Entrez un intervalle d'au moins 1 heure.", + 'timing.invalidInterval.minute': "Entrez un intervalle d'au moins 1 minute.", + 'timing.invalidInterval.second': "Entrez un intervalle d'au moins 60 secondes.", + 'timing.error': 'Impossible de confirmer la mise à jour du timing. Votre brouillon est conservé. Vérifiez la tâche avant de réessayer.', + 'rule.title': "Temps d'exécution", + 'rule.repeat': 'Répéter', + 'rule.weekday': 'Jour de la semaine', + 'rule.weekdayOption': 'Jour de la semaine {weekday}', + 'rule.unsaved': 'Modifications non enregistrées', + 'rule.save': 'Enregistrer les modifications', + 'rule.saving': 'Sauvegarde…', + 'rule.cancel': 'Annuler', + 'rule.invalidTitle': 'Saisissez un nom de tâche comportant au maximum 120 caractères.', + 'rule.invalidPrompt': 'Entrez une instruction de tâche.', + 'rule.once': 'Une fois', + 'rule.everyMinutes': 'Toutes les N minutes', + 'rule.everyHours': 'Toutes les N heures', + 'rule.everySeconds': 'Toutes les N secondes', + 'rule.daily': 'Chaque jour', + 'rule.weekdays': 'Du lundi au vendredi', + 'rule.weekly': 'Hebdomadaire', + 'rule.cron': 'Personnalisé', + 'rule.cronLabel': 'Expression Cron', + 'rule.cronInvalid': 'Entrez une expression cron à cinq champs, par exemple 0 9 * * 1-5.', + 'cronForm.frequency': 'Fréquence', + 'cronForm.monthly': 'Mensuel', + 'cronForm.weekly': 'Hebdomadaire', + 'cronForm.daily': 'Chaque jour', + 'cronForm.dates': 'Aux dates', + 'cronForm.dateOption': 'Jour {day}', + 'cronForm.atMinute': 'À la minute', + 'cronForm.minuteIncrease': 'Augmenter les minutes', + 'cronForm.minuteDecrease': 'Diminuer les minutes', + 'rule.zone.system': ' (système)', + 'rule.error.conflict': "Cette tâche a changé avant l'enregistrement de la mise à jour. La ligne affiche la règle enregistrée ; essayer à nouveau.", + 'rule.error.notFound': "Cette tâche n'est plus disponible.", + 'rule.error.unknown': 'Impossible de confirmer la mise à jour de la règle. La ligne affiche la règle enregistrée.', + 'delete.action': 'Supprimer la tâche', + 'delete.title': 'Supprimer cette tâche ?', + 'delete.description': 'Cela empêche le déclenchement de la tâche et la supprime ainsi que ses enregistrements de livraison enregistrés. ' + + "La session d'origine et ses messages restent conservés ; les messages en file d'attente ne sont pas retirés.", + 'delete.confirm': 'Confirmer la suppression', + 'delete.cancel': 'Annuler', + 'delete.close': 'Fermer la confirmation de suppression', + 'delete.pending': 'Suppression…', + 'toast.deleted': 'Tâche supprimée.', + 'toast.deleteFailed': 'Impossible de supprimer la tâche.', + 'card.open': 'Ouvrir', + 'card.openLabel': 'Détails de la tâche ouverte : {title}', + 'card.deleted': 'Supprimé', + 'tool.invoked': '{name} invoqué', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'panel': 'Automatisierungsaufgaben', + 'title': 'Automatisierungsaufgaben', + 'new.action': 'Neu', + 'list.label': 'Aufgabenkatalog', + 'list.nextPrefix': 'Nächste geplante Zeit: ', + 'list.loading': 'Aufgaben werden geladen…', + 'list.error': 'Aufgaben konnten nicht geladen werden.', + 'list.retry': 'Erneut versuchen', + 'list.empty': 'Noch keine Aufgaben. Hier werden die in Ihren Sitzungen erstellten Aufgaben angezeigt.', + 'list.emptyInactive': 'Keine inaktiven Automatisierungsaufgaben', + 'list.noMatches': 'Keine passenden Aufgaben', + 'search.label': 'Suchaufgaben', + 'search.placeholder': 'Suchautomatisierungsaufgaben', + 'search.clear': 'Suche löschen', + 'empty.action': 'Neue Automatisierungsaufgabe', + 'statusFilter.label': 'Aufgabenstatus', + 'statusFilter.all': 'Alle', + 'status.active': 'Aktiviert', + 'status.inactive': 'Inaktiv', + 'detail.label': 'Aufgabendetails', + 'detail.close': 'Details schließen', + 'detail.more': 'Aktionen der Automatisierungsaufgabe', + 'detail.nextRun': 'Nächster Lauf', + 'detail.tabs': 'Aufgabendetailansichten', + 'detail.rule': 'Regeln', + 'detail.records': 'Aufzeichnungen', + 'delivery.empty': 'Kein Lieferprotokoll verfügbar', + 'delivery.loading': 'Lieferdatensätze werden geladen…', + 'delivery.error': 'Lieferdatensätze konnten nicht geladen werden.', + 'delivery.notFound': 'Diese Aufgabe ist nicht mehr verfügbar.', + 'delivery.cursorError': 'Die Datensätze wurden aktualisiert.', + 'delivery.retry': 'Erneut versuchen', + 'delivery.refresh': 'Aktualisieren', + 'delivery.loadMore': 'Mehr laden', + 'delivery.expand': 'Ausklappen', + 'delivery.collapse': 'Einklappen', + 'delivery.pruned': 'Frühere Lieferaufzeichnungen wurden gelöscht', + 'delivery.retention': 'Aufbewahrungsregeln', + 'delivery.retentionBounds': 'Jede Aufgabe verwaltet bis zu {records}-Lieferaufzeichnungen der letzten {days}-Tage.', + 'delivery.retentionExplanation': 'Wenn ein neuer Datensatz gespeichert wird, werden ältere Datensätze außerhalb dieser Grenzen automatisch gelöscht. Durch das Löschen von Datensätzen wird die Aufgabe nicht gestoppt.', + 'detail.name': 'Name der Automatisierungsaufgabe', + 'detail.instruction': 'Anleitung zur Automatisierungsaufgabe', + 'detail.status': 'Status', + 'detail.next': 'Nächste geplante Zeit', + 'detail.frequency': 'Häufigkeit', + 'detail.id': 'Aufgaben-ID', + 'detail.session': 'Verknüpfte Sitzung', + 'detail.openSession': 'Verknüpfte Sitzung: Originalsitzung öffnen', + 'detail.openSessionTitle': 'Verknüpfte Sitzung {title}: Originalsitzung öffnen', + 'detail.sessionLoading': 'Laden der ursprünglichen Sitzungsinformationen.', + 'detail.sessionArchived': 'Die ursprüngliche Sitzung wird archiviert.', + 'detail.sessionUnavailable': 'Die ursprüngliche Sitzung ist nicht verfügbar.', + 'detail.missing': 'Aufgabe nicht verfügbar. Möglicherweise wurde es gelöscht.', + 'delivery.label': 'Lieferdatensatz gespeichert', + 'frequency.once': 'Einmal', + 'frequency.every': 'Jeder {value} {unit}', + ...frequencyDe, + 'timing.date': 'Datum', + 'timing.time': 'Zeit', + 'timing.hour': 'Stunde', + 'timing.minute': 'Minute', + 'timing.second': 'Sekunde', + 'timing.prevMonth': 'Vorheriger Monat', + 'timing.nextMonth': 'Nächsten Monat', + 'timing.zone': 'Zeitzone', + 'timing.zoneSearch': 'Suche nach UTC-Offset, IANA-ID oder Stadt', + 'timing.zoneNoResults': 'Keine passenden Zeitzonen', + 'timing.interval': 'Wiederholen Sie alle', + 'timing.intervalIncrease': 'Intervall erhöhen', + 'timing.intervalDecrease': 'Intervall verringern', + 'timing.unit.hour': 'Stunden', + 'timing.unit.minute': 'Minuten', + 'timing.unit.second': 'Sekunden', + 'timing.intervalHint.hour': 'Mindestens 1 Stunde. Das Intervall zählt ab der Aufgabenerstellung oder der letzten Regeländerung und ist unabhängig von Zeitzonen.', + 'timing.intervalHint.minute': 'Mindestens 1 Minute. Das Intervall zählt ab der Aufgabenerstellung oder der letzten Regeländerung und ist unabhängig von Zeitzonen.', + 'timing.intervalHint.second': 'Mindestens 60 Sekunden. Das Intervall zählt ab der Aufgabenerstellung oder der letzten Regeländerung und ist unabhängig von Zeitzonen.', + 'timing.zoneNoStored': 'Eine einmalige Aufgabe speichert nur ihren Zielzeitpunkt, keine Zeitzone. Datum und Uhrzeit werden in der gewählten Zeitzone interpretiert.', + 'timing.inactive': 'Inaktive Aufgaben sind schreibgeschützt. Ihr Timing kann nicht bearbeitet werden.', + 'timing.conflict': 'Diese Aufgabe hat sich während der Bearbeitung geändert. Ihr Entwurf bleibt erhalten. ' + + 'Abbrechen und nach der Aktualisierung erneut öffnen, um die neueste Regel zu bearbeiten.', + 'timing.notFound': 'Diese Aufgabe ist nicht mehr verfügbar. Ihr Entwurf bleibt erhalten; Abbrechen, um den Editor zu schließen.', + 'timing.subagentSession': 'Diese Sitzung gehört einem Subagenten, der niemals zugestellte Erinnerungen erhält.', + 'timing.invalid': 'Geben Sie ein gültiges Datum und eine gültige Uhrzeit ein oder überprüfen Sie die Zeitfelder.', + 'timing.invalidZone': 'Geben Sie eine gültige IANA-Zeitzone ein, zum Beispiel Asien/Shanghai.', + 'timing.notFuture': 'Wählen Sie ein Datum und eine Uhrzeit in der Zukunft.', + 'timing.invalidInterval': 'Geben Sie ein Intervall von mindestens 1 Minute ein.', + 'timing.invalidInterval.hour': 'Geben Sie ein Intervall von mindestens 1 Stunde ein.', + 'timing.invalidInterval.minute': 'Geben Sie ein Intervall von mindestens 1 Minute ein.', + 'timing.invalidInterval.second': 'Geben Sie ein Intervall von mindestens 60 Sekunden ein.', + 'timing.error': 'Das Timing-Update konnte nicht bestätigt werden. Ihr Entwurf bleibt erhalten. Überprüfen Sie die Aufgabe, bevor Sie es erneut versuchen.', + 'rule.title': 'Laufzeit', + 'rule.repeat': 'Wiederholen', + 'rule.weekday': 'Wochentag', + 'rule.weekdayOption': 'Wochentag {weekday}', + 'rule.unsaved': 'Nicht gespeicherte Änderungen', + 'rule.save': 'Änderungen speichern', + 'rule.saving': 'Speichern…', + 'rule.cancel': 'Abbrechen', + 'rule.invalidTitle': 'Geben Sie einen Aufgabennamen mit maximal 120 Zeichen ein.', + 'rule.invalidPrompt': 'Geben Sie eine Aufgabenanweisung ein.', + 'rule.once': 'Einmal', + 'rule.everyMinutes': 'Alle N Minuten', + 'rule.everyHours': 'Alle N Stunden', + 'rule.everySeconds': 'Alle N Sekunden', + 'rule.daily': 'Jeden Tag', + 'rule.weekdays': 'Montag bis Freitag', + 'rule.weekly': 'Wöchentlich', + 'rule.cron': 'Benutzerdefiniert', + 'rule.cronLabel': 'Cron-Ausdruck', + 'rule.cronInvalid': 'Geben Sie einen Cron-Ausdruck mit fünf Feldern ein, zum Beispiel 0 9 * * 1-5.', + 'cronForm.frequency': 'Häufigkeit', + 'cronForm.monthly': 'Monatlich', + 'cronForm.weekly': 'Wöchentlich', + 'cronForm.daily': 'Jeden Tag', + 'cronForm.dates': 'An Terminen', + 'cronForm.dateOption': 'Tag {day}', + 'cronForm.atMinute': 'Im Moment', + 'cronForm.minuteIncrease': 'Minute erhöhen', + 'cronForm.minuteDecrease': 'Minute verringern', + 'rule.zone.system': ' (System)', + 'rule.error.conflict': 'Diese Aufgabe wurde vor dem Speichern des Updates geändert. Die Zeile zeigt die gespeicherte Regel; versuchen Sie es erneut.', + 'rule.error.notFound': 'Diese Aufgabe ist nicht mehr verfügbar.', + 'rule.error.unknown': 'Die Regelaktualisierung konnte nicht bestätigt werden. Die Zeile zeigt die gespeicherte Regel.', + 'delete.action': 'Aufgabe löschen', + 'delete.title': 'Diese Aufgabe löschen?', + 'delete.description': 'Dadurch wird verhindert, dass die Aufgabe ausgelöst wird, und sie wird zusammen mit den gespeicherten Lieferdatensätzen gelöscht. ' + + 'Die ursprüngliche Sitzung und ihre Nachrichten bleiben erhalten; Nachrichten in der Warteschlange werden nicht zurückgezogen.', + 'delete.confirm': 'Bestätigen Sie den Löschvorgang', + 'delete.cancel': 'Abbrechen', + 'delete.close': 'Löschbestätigung schließen', + 'delete.pending': 'Löschen…', + 'toast.deleted': 'Aufgabe gelöscht.', + 'toast.deleteFailed': 'Die Aufgabe konnte nicht gelöscht werden.', + 'card.open': 'Öffnen', + 'card.openLabel': 'Aufgabendetails öffnen: {title}', + 'card.deleted': 'Gelöscht', + 'tool.invoked': '{name} aufgerufen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'panel': 'Задачи автоматизации', + 'title': 'Задачи автоматизации', + 'new.action': 'Новый', + 'list.label': 'Каталог задач', + 'list.nextPrefix': 'Следующее запланированное время: ', + 'list.loading': 'Загрузка задач…', + 'list.error': 'Не удалось загрузить задачи.', + 'list.retry': 'Повторить', + 'list.empty': 'Заданий пока нет. Здесь отображаются задачи, созданные в ваших сеансах.', + 'list.emptyInactive': 'Нет неактивных задач автоматизации', + 'list.noMatches': 'Нет подходящих задач', + 'search.label': 'Поиск задач', + 'search.placeholder': 'Задачи автоматизации поиска', + 'search.clear': 'Очистить поиск', + 'empty.action': 'Новая задача автоматизации', + 'statusFilter.label': 'Статус задачи', + 'statusFilter.all': 'Все', + 'status.active': 'Включено', + 'status.inactive': 'Неактивный', + 'detail.label': 'Подробности задачи', + 'detail.close': 'Закрыть детали', + 'detail.more': 'Действия задачи автоматизации', + 'detail.nextRun': 'Следующий запуск', + 'detail.tabs': 'Просмотр сведений о задаче', + 'detail.rule': 'Правила', + 'detail.records': 'Рекорды', + 'delivery.empty': 'Нет записи о доставке', + 'delivery.loading': 'Загрузка записей о доставке…', + 'delivery.error': 'Не удалось загрузить записи о доставке.', + 'delivery.notFound': 'Эта задача больше не доступна.', + 'delivery.cursorError': 'Записи обновлены.', + 'delivery.retry': 'Повторить', + 'delivery.refresh': 'Обновить', + 'delivery.loadMore': 'Загрузить больше', + 'delivery.expand': 'Развернуть', + 'delivery.collapse': 'Свернуть', + 'delivery.pruned': 'Записи о предыдущих поставках удалены.', + 'delivery.retention': 'Правила хранения', + 'delivery.retentionBounds': 'Каждая задача сохраняет записи о доставке до {records} за последние дни {days}.', + 'delivery.retentionExplanation': 'При сохранении новой записи более старые записи, выходящие за эти пределы, автоматически удаляются. Очистка записей не останавливает выполнение задачи.', + 'detail.name': 'Название задачи автоматизации', + 'detail.instruction': 'Инструкция по задаче автоматизации', + 'detail.status': 'Статус', + 'detail.next': 'Следующее запланированное время', + 'detail.frequency': 'Частота', + 'detail.id': 'Идентификатор задачи', + 'detail.session': 'Связанный сеанс', + 'detail.openSession': 'Связанный сеанс: открыть исходный сеанс.', + 'detail.openSessionTitle': 'Связанный сеанс {title}: открыть исходный сеанс.', + 'detail.sessionLoading': 'Загрузка исходной информации о сеансе.', + 'detail.sessionArchived': 'Исходный сеанс архивируется.', + 'detail.sessionUnavailable': 'Исходный сеанс недоступен.', + 'detail.missing': 'Задача недоступна. Возможно, оно было удалено.', + 'delivery.label': 'Сохраненная запись о доставке', + 'frequency.once': 'Однажды', + 'frequency.every': 'Каждый {value} {unit}', + ...frequencyRu, + 'timing.date': 'Дата', + 'timing.time': 'Время', + 'timing.hour': 'Час', + 'timing.minute': 'минута', + 'timing.second': 'Секунда', + 'timing.prevMonth': 'Предыдущий месяц', + 'timing.nextMonth': 'В следующем месяце', + 'timing.zone': 'Часовой пояс', + 'timing.zoneSearch': 'Поиск по смещению UTC, идентификатору IANA или городу.', + 'timing.zoneNoResults': 'Нет подходящих часовых поясов', + 'timing.interval': 'Повторяйте каждые', + 'timing.intervalIncrease': 'Увеличить интервал', + 'timing.intervalDecrease': 'Уменьшить интервал', + 'timing.unit.hour': 'часы', + 'timing.unit.minute': 'минуты', + 'timing.unit.second': 'секунды', + 'timing.intervalHint.hour': 'Минимум 1 час. Интервал отсчитывается с момента создания задачи или последнего изменения правила и не зависит от часовых поясов.', + 'timing.intervalHint.minute': 'Минимум 1 минута. Интервал отсчитывается с момента создания задачи или последнего изменения правила и не зависит от часовых поясов.', + 'timing.intervalHint.second': 'Минимум 60 секунд. Интервал отсчитывается с момента создания задачи или последнего изменения правила и не зависит от часовых поясов.', + 'timing.zoneNoStored': 'Одноразовая задача сохраняет только целевой момент, а не часовой пояс. Дата и время интерпретируются в выбранном часовом поясе.', + 'timing.inactive': 'Неактивные задачи доступны только для чтения. Их время не может быть отредактировано.', + 'timing.conflict': 'Эта задача изменилась во время редактирования. Ваш черновик сохранен. ' + + 'Отмените и снова откройте после обновления, чтобы изменить последнее правило.', + 'timing.notFound': 'Эта задача больше не доступна. Ваш черновик сохраняется; отмените, чтобы закрыть редактор.', + 'timing.subagentSession': 'Этот сеанс принадлежит субагенту, который никогда не получает напоминания.', + 'timing.invalid': 'Введите действительные дату и время или проверьте поля времени.', + 'timing.invalidZone': 'Введите действительный часовой пояс IANA, например Азия/Шанхай.', + 'timing.notFuture': 'Выберите дату и время в будущем.', + 'timing.invalidInterval': 'Введите интервал не менее 1 минуты.', + 'timing.invalidInterval.hour': 'Введите интервал не менее 1 часа.', + 'timing.invalidInterval.minute': 'Введите интервал не менее 1 минуты.', + 'timing.invalidInterval.second': 'Введите интервал не менее 60 секунд.', + 'timing.error': 'Не удалось подтвердить обновление времени. Ваш черновик сохранен. Проверьте задачу, прежде чем повторить попытку.', + 'rule.title': 'Время выполнения', + 'rule.repeat': 'Повторить', + 'rule.weekday': 'Будний день', + 'rule.weekdayOption': 'День недели {weekday}', + 'rule.unsaved': 'Несохраненные изменения', + 'rule.save': 'Сохранить изменения', + 'rule.saving': 'Сохранение…', + 'rule.cancel': 'Отмена', + 'rule.invalidTitle': 'Введите имя задачи длиной не более 120 символов.', + 'rule.invalidPrompt': 'Введите инструкцию задачи.', + 'rule.once': 'Однажды', + 'rule.everyMinutes': 'Каждые N минут', + 'rule.everyHours': 'Каждые N часов', + 'rule.everySeconds': 'Каждые N секунд', + 'rule.daily': 'Каждый день', + 'rule.weekdays': 'С понедельника по пятницу', + 'rule.weekly': 'Еженедельно', + 'rule.cron': 'Пользовательский', + 'rule.cronLabel': 'Выражение Cron', + 'rule.cronInvalid': 'Введите выражение cron из пяти полей, например 0 9 * * 1-5.', + 'cronForm.frequency': 'Частота', + 'cronForm.monthly': 'Ежемесячно', + 'cronForm.weekly': 'Еженедельно', + 'cronForm.daily': 'Каждый день', + 'cronForm.dates': 'По датам', + 'cronForm.dateOption': 'День {day}', + 'cronForm.atMinute': 'В минуту', + 'cronForm.minuteIncrease': 'Увеличить минуту', + 'cronForm.minuteDecrease': 'Уменьшить минуту', + 'rule.zone.system': ' (система)', + 'rule.error.conflict': 'Эта задача изменилась до сохранения обновления. В строке отображается сохраненное правило; Попробуйте еще раз.', + 'rule.error.notFound': 'Эта задача больше не доступна.', + 'rule.error.unknown': 'Не удалось подтвердить обновление правила. В строке отображается сохраненное правило.', + 'delete.action': 'Удалить задачу', + 'delete.title': 'Удалить эту задачу?', + 'delete.description': 'Это останавливает запуск задачи и удаляет ее вместе с сохраненными записями о доставке. ' + + 'Исходный сеанс и его сообщения остаются; сообщения в очереди не изымаются.', + 'delete.confirm': 'Подтвердить удаление', + 'delete.cancel': 'Отмена', + 'delete.close': 'Закрыть подтверждение удаления', + 'delete.pending': 'Удаление…', + 'toast.deleted': 'Задача удалена.', + 'toast.deleteFailed': 'Не удалось удалить задачу.', + 'card.open': 'Открыть', + 'card.openLabel': 'Открыть сведения о задаче: {title}.', + 'card.deleted': 'Удалено', + 'tool.invoked': 'Вызван {name}', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'panel': 'Tarefas de automação', + 'title': 'Tarefas de automação', + 'new.action': 'Novo', + 'list.label': 'Catálogo de tarefas', + 'list.nextPrefix': 'Próximo horário agendado: ', + 'list.loading': 'Carregando tarefas…', + 'list.error': 'Não foi possível carregar tarefas.', + 'list.retry': 'Tentar novamente', + 'list.empty': 'Nenhuma tarefa ainda. As tarefas criadas nas suas sessões aparecem aqui.', + 'list.emptyInactive': 'Nenhuma tarefa de automação inativa', + 'list.noMatches': 'Nenhuma tarefa correspondente', + 'search.label': 'Tarefas de pesquisa', + 'search.placeholder': 'Tarefas de automação de pesquisa', + 'search.clear': 'Limpar pesquisa', + 'empty.action': 'Nova tarefa de automação', + 'statusFilter.label': 'Status da tarefa', + 'statusFilter.all': 'Todos', + 'status.active': 'Habilitado', + 'status.inactive': 'Inativo', + 'detail.label': 'Detalhes da tarefa', + 'detail.close': 'Fechar detalhes', + 'detail.more': 'Ações de tarefas de automação', + 'detail.nextRun': 'Próxima execução', + 'detail.tabs': 'Visualizações de detalhes da tarefa', + 'detail.rule': 'Regras', + 'detail.records': 'Registros', + 'delivery.empty': 'Nenhum registro de entrega disponível', + 'delivery.loading': 'Carregando registros de entrega…', + 'delivery.error': 'Não foi possível carregar os registros de entrega.', + 'delivery.notFound': 'Esta tarefa não está mais disponível.', + 'delivery.cursorError': 'Os registros foram atualizados.', + 'delivery.retry': 'Tentar novamente', + 'delivery.refresh': 'Atualizar', + 'delivery.loadMore': 'Carregar mais', + 'delivery.expand': 'Expandir', + 'delivery.collapse': 'Recolher', + 'delivery.pruned': 'Registros de entrega anteriores foram apagados', + 'delivery.retention': 'Regras de retenção', + 'delivery.retentionBounds': 'Cada tarefa mantém até {records} registros de entrega dos últimos {days} dias.', + 'delivery.retentionExplanation': 'Quando um novo registro é salvo, os registros mais antigos fora desses limites são apagados automaticamente. A limpeza de registros não interrompe a tarefa.', + 'detail.name': 'Nome da tarefa de automação', + 'detail.instruction': 'Instrução de tarefa de automação', + 'detail.status': 'Estado', + 'detail.next': 'Próximo horário agendado', + 'detail.frequency': 'Frequência', + 'detail.id': 'ID da tarefa', + 'detail.session': 'Sessão vinculada', + 'detail.openSession': 'Sessão vinculada: sessão original aberta', + 'detail.openSessionTitle': 'Sessão vinculada {title}: sessão original aberta', + 'detail.sessionLoading': 'Carregando informações originais da sessão.', + 'detail.sessionArchived': 'A sessão original é arquivada.', + 'detail.sessionUnavailable': 'A sessão original não está disponível.', + 'detail.missing': 'Tarefa indisponível. Pode ter sido excluído.', + 'delivery.label': 'Registro de entrega salvo', + 'frequency.once': 'Uma vez', + 'frequency.every': 'A cada {value} {unit}', + ...frequencyPt, + 'timing.date': 'Data', + 'timing.time': 'Hora', + 'timing.hour': 'Hora', + 'timing.minute': 'Minuto', + 'timing.second': 'Segundo', + 'timing.prevMonth': 'Mês anterior', + 'timing.nextMonth': 'No próximo mês', + 'timing.zone': 'Fuso horário', + 'timing.zoneSearch': 'Pesquise por deslocamento UTC, ID IANA ou cidade', + 'timing.zoneNoResults': 'Nenhum fuso horário correspondente', + 'timing.interval': 'Repita cada', + 'timing.intervalIncrease': 'Aumentar intervalo', + 'timing.intervalDecrease': 'Diminuir intervalo', + 'timing.unit.hour': 'horas', + 'timing.unit.minute': 'minutos', + 'timing.unit.second': 'segundos', + 'timing.intervalHint.hour': 'Pelo menos 1 hora. O intervalo conta a partir da criação da tarefa ou da última alteração de regra e é independente dos fusos horários.', + 'timing.intervalHint.minute': 'Pelo menos 1 minuto. O intervalo conta a partir da criação da tarefa ou da última alteração de regra e é independente dos fusos horários.', + 'timing.intervalHint.second': 'Pelo menos 60 segundos. O intervalo conta a partir da criação da tarefa ou da última alteração de regra e é independente dos fusos horários.', + 'timing.zoneNoStored': 'Uma tarefa única armazena apenas o momento alvo, não um fuso horário. A data e a hora são interpretadas no fuso horário selecionado.', + 'timing.inactive': 'As tarefas inativas são somente leitura. O tempo deles não pode ser editado.', + 'timing.conflict': 'Esta tarefa mudou enquanto você estava editando. Seu rascunho foi mantido. ' + + 'Cancele e reabra após a atualização para editar a regra mais recente.', + 'timing.notFound': 'Esta tarefa não está mais disponível. Seu rascunho foi retido; cancelar para fechar o editor.', + 'timing.subagentSession': 'Esta sessão pertence a um subagente, que nunca recebe lembretes entregues.', + 'timing.invalid': 'Insira uma data e hora válidas ou verifique os campos de horário.', + 'timing.invalidZone': 'Insira um fuso horário IANA válido, por exemplo Ásia/Xangai.', + 'timing.notFuture': 'Escolha uma data e hora no futuro.', + 'timing.invalidInterval': 'Insira um intervalo de pelo menos 1 minuto.', + 'timing.invalidInterval.hour': 'Insira um intervalo de pelo menos 1 hora.', + 'timing.invalidInterval.minute': 'Insira um intervalo de pelo menos 1 minuto.', + 'timing.invalidInterval.second': 'Insira um intervalo de pelo menos 60 segundos.', + 'timing.error': 'Não foi possível confirmar a atualização de tempo. Seu rascunho foi mantido. Verifique a tarefa antes de tentar novamente.', + 'rule.title': 'Tempo de execução', + 'rule.repeat': 'Repetir', + 'rule.weekday': 'Dia da semana', + 'rule.weekdayOption': 'Dia da semana {weekday}', + 'rule.unsaved': 'Alterações não salvas', + 'rule.save': 'Salvar alterações', + 'rule.saving': 'Salvando…', + 'rule.cancel': 'Cancelar', + 'rule.invalidTitle': 'Insira um nome de tarefa com no máximo 120 caracteres.', + 'rule.invalidPrompt': 'Insira uma instrução de tarefa.', + 'rule.once': 'Uma vez', + 'rule.everyMinutes': 'A cada N minutos', + 'rule.everyHours': 'A cada N horas', + 'rule.everySeconds': 'A cada N segundos', + 'rule.daily': 'Todos os dias', + 'rule.weekdays': 'Segunda a sexta', + 'rule.weekly': 'Semanalmente', + 'rule.cron': 'Personalizado', + 'rule.cronLabel': 'Expressão Cron', + 'rule.cronInvalid': 'Insira uma expressão cron de cinco campos, por exemplo 0 9 * * 1-5.', + 'cronForm.frequency': 'Frequência', + 'cronForm.monthly': 'Mensalmente', + 'cronForm.weekly': 'Semanalmente', + 'cronForm.daily': 'Todos os dias', + 'cronForm.dates': 'Em datas', + 'cronForm.dateOption': 'Dia {day}', + 'cronForm.atMinute': 'No minuto', + 'cronForm.minuteIncrease': 'Aumentar minuto', + 'cronForm.minuteDecrease': 'Diminuir minuto', + 'rule.zone.system': ' (sistema)', + 'rule.error.conflict': 'Esta tarefa foi alterada antes da atualização ser salva. A linha mostra a regra salva; tente novamente.', + 'rule.error.notFound': 'Esta tarefa não está mais disponível.', + 'rule.error.unknown': 'Não foi possível confirmar a atualização da regra. A linha mostra a regra salva.', + 'delete.action': 'Excluir tarefa', + 'delete.title': 'Excluir esta tarefa?', + 'delete.description': 'Isso interrompe o acionamento da tarefa e a exclui junto com seus registros de entrega salvos. ' + + 'A sessão original e suas mensagens permanecem; mensagens enfileiradas não são retiradas.', + 'delete.confirm': 'Confirmar exclusão', + 'delete.cancel': 'Cancelar', + 'delete.close': 'Fechar confirmação de exclusão', + 'delete.pending': 'Excluindo…', + 'toast.deleted': 'Tarefa excluída.', + 'toast.deleteFailed': 'Não foi possível excluir a tarefa.', + 'card.open': 'Abrir', + 'card.openLabel': 'Detalhes da tarefa aberta: {title}', + 'card.deleted': 'Excluído', + 'tool.invoked': 'Invocado {name}', +} satisfies Record diff --git a/packages/client/ui-schedule/tests/browser-plugin.client.spec.ts b/packages/client/ui-schedule/tests/browser-plugin.client.spec.ts index 768c478bef..5ae0ea5ac2 100644 --- a/packages/client/ui-schedule/tests/browser-plugin.client.spec.ts +++ b/packages/client/ui-schedule/tests/browser-plugin.client.spec.ts @@ -11,7 +11,9 @@ import { apply as applyNode } from '../src/index.ts' import type { SessionId } from '@deepseek-ai/dsh-session/types' import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/client' import type { WorkspaceSnapshot } from '@deepseek-ai/dsh-api-workspace-controller/client' -import type { ScheduleId, ScheduleCatalogEntry, ScheduleUpdateRequest, ScheduleUpdateResult } from '@deepseek-ai/dsh-schedule/client' +import type { + ScheduleId, ScheduleCatalogEntry, ScheduleDeleteInactiveResult, ScheduleUpdateRequest, ScheduleUpdateResult, +} from '@deepseek-ai/dsh-schedule/client' import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client' import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime' import type { CatalogInjected } from '../src/client/catalog-source.ts' @@ -496,6 +498,83 @@ describe('ui-schedule browser half', () => { } }) + it('clears every ended task from the global page, reads the catalog back, and reports the count', async () => { + const record: ScheduleCatalogEntry = { + id: 'host-ended' as ScheduleId, sessionId: 'cold-original' as SessionId, status: 'inactive', + kind: 'after', title: 'Ended review', prompt: 'Ended review', afterSeconds: 60, + scheduledAt: '2099-01-01T00:00:00.000Z', + } + const catalog = vi.fn() + .mockResolvedValueOnce({ ok: true, value: [record] }) + .mockResolvedValue({ ok: true, value: [] }) + const deleteInactive = vi.fn().mockResolvedValue({ ok: true, value: { deleted: 1 } }) + const ctx = await baseContext({ schedule: { catalog, deleteInactive }, $on: () => () => {} }) + const owner = ctx.slots.register({ + name: 'root', + children: { + main: { kind: 'keyed', scope: 'root' }, + 'shell.overlay': { kind: 'list', scope: 'root' }, + }, + } as never, Empty) + const fiber = ctx.plugin({ inject: [...inject], apply }) + await fiber.await() + try { + const face = injectedFace(ctx.slots.entries('main')[0]!, undefined as never) as TaskManagerInjected + const toast = injectedFace(ctx.slots.entries('shell.overlay')[0]!) as Omit + const dispose = face.hooks.catalog.subscribe(vi.fn()) + try { + await vi.waitFor(() => { expect(face.hooks.catalog.getSnapshot().records).toEqual([record]) }) + const reads = catalog.mock.calls.length + await face.onClearEnded() + expect(deleteInactive).toHaveBeenCalledOnce() + // The acknowledged removal reads the catalog back instead of joining a + // read issued before it, so the page's rows follow the removal. + expect(catalog.mock.calls.length).toBe(reads + 1) + await vi.waitFor(() => { expect(face.hooks.catalog.getSnapshot().records).toEqual([]) }) + expect(toast.hooks.toast.getSnapshot()).toEqual({ kind: 'cleared', count: 1, seq: 1 }) + } finally { + dispose() + } + } finally { + await fiber.dispose() + owner() + await ctx.fiber.dispose() + } + }) + + it('reports a settled bulk removal after the page registration is gone', async () => { + const catalog = vi.fn().mockResolvedValue({ ok: true, value: [] }) + const removal = Promise.withResolvers<{ ok: true; value: ScheduleDeleteInactiveResult }>() + const deleteInactive = vi.fn(() => removal.promise) + const ctx = await baseContext({ schedule: { catalog, deleteInactive }, $on: () => () => {} }) + const owner = ctx.slots.register({ + name: 'root', + children: { + main: { kind: 'keyed', scope: 'root' }, + 'shell.overlay': { kind: 'list', scope: 'root' }, + }, + } as never, Empty) + const fiber = ctx.plugin({ inject: [...inject], apply }) + await fiber.await() + let claimed = false + try { + const face = injectedFace(ctx.slots.entries('main')[0]!, undefined as never) as TaskManagerInjected + const toast = injectedFace(ctx.slots.entries('shell.overlay')[0]!) as Omit + const clearing = face.onClearEnded() + // The page panel can unmount while the Host removal is still in flight; + // the notice store belongs to the plugin, so the outcome still publishes. + owner() + claimed = true + removal.resolve({ ok: true, value: { deleted: 2 } }) + await clearing + expect(toast.hooks.toast.getSnapshot()).toEqual({ kind: 'cleared', count: 2, seq: 1 }) + } finally { + if (!claimed) owner() + await fiber.dispose() + await ctx.fiber.dispose() + } + }) + it('registers the row mark and hover-card seats under the Session row declarations', async () => { const catalog = vi.fn().mockResolvedValue({ ok: true, value: [] }) const ctx = await baseContext({ schedule: { catalog }, $on: () => () => {} }) diff --git a/packages/client/ui-schedule/tests/clear-ended.client.spec.ts b/packages/client/ui-schedule/tests/clear-ended.client.spec.ts new file mode 100644 index 0000000000..ef943688b0 --- /dev/null +++ b/packages/client/ui-schedule/tests/clear-ended.client.spec.ts @@ -0,0 +1,74 @@ +/** The management page's bulk removal: one request, one readback, one settled outcome. */ +import { describe, expect, it, vi } from 'vitest' +import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime' +import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client' +import type { ScheduleDeleteInactiveResult } from '@deepseek-ai/dsh-schedule/client' +import { createClearEnded } from '../src/client/clear-ended.ts' + +const failure: RemoteResult = { + ok: false, error: new RemoteError('gateway/internal', 'Unavailable', {}), +} + +/** + * Wire the helper to one fake Remote answer and one readback spy. + * @param answer - what the removal call answers, or the reason it rejects. + * @param refresh - the readback the helper calls after an acknowledged removal. + * @returns the callback under test with its dependencies' spies. + */ +function setup( + answer: RemoteResult | Error, + refresh = vi.fn(async () => {}), +) { + const removeInactive = vi.fn(async () => { + if (answer instanceof Error) throw answer + return answer + }) + return { clearEnded: createClearEnded({ removeInactive, refresh }), removeInactive, refresh } +} + +describe('createClearEnded', () => { + it('reads the catalog back before the outcome settles and reports how many rows went', async () => { + const readback = Promise.withResolvers() + const { clearEnded, removeInactive, refresh } = setup( + { ok: true, value: { deleted: 3 } }, + vi.fn(() => readback.promise), + ) + let settled = false + const outcome = clearEnded().then((value) => { + settled = true + return value + }) + + await vi.waitFor(() => { expect(refresh).toHaveBeenCalledOnce() }) + // The list must agree with the count before the notice states it, so the + // outcome waits for the readback rather than publishing alongside it. + expect(settled).toBe(false) + readback.resolve(undefined) + + await expect(outcome).resolves.toEqual({ cleared: true, count: 3 }) + expect(removeInactive).toHaveBeenCalledOnce() + expect(removeInactive.mock.invocationCallOrder[0]) + .toBeLessThan(refresh.mock.invocationCallOrder[0]!) + }) + + it('reports an acknowledged removal that found nothing as a cleared outcome', async () => { + const { clearEnded, refresh } = setup({ ok: true, value: { deleted: 0 } }) + + await expect(clearEnded()).resolves.toEqual({ cleared: true, count: 0 }) + expect(refresh).toHaveBeenCalledOnce() + }) + + it('reports a refused removal and reads nothing back', async () => { + const { clearEnded, refresh } = setup(failure) + + await expect(clearEnded()).resolves.toEqual({ cleared: false }) + expect(refresh).not.toHaveBeenCalled() + }) + + it('reports a rejected removal and reads nothing back', async () => { + const { clearEnded, refresh } = setup(new Error('connection lost')) + + await expect(clearEnded()).resolves.toEqual({ cleared: false }) + expect(refresh).not.toHaveBeenCalled() + }) +}) diff --git a/packages/client/ui-schedule/tests/delete-toast.client.spec.tsx b/packages/client/ui-schedule/tests/delete-toast.client.spec.tsx index 55f6535587..7112670eea 100644 --- a/packages/client/ui-schedule/tests/delete-toast.client.spec.tsx +++ b/packages/client/ui-schedule/tests/delete-toast.client.spec.tsx @@ -48,6 +48,17 @@ describe('delete toast source', () => { source.report('deleted') expect(listener).toHaveBeenCalledTimes(4) }) + + it('publishes each settled bulk removal with its count or failure', () => { + const source = createDeleteToastSource() + source.reportClear({ cleared: true, count: 3 }) + expect(source.hooks.toast.getSnapshot()).toEqual({ kind: 'cleared', count: 3, seq: 1 }) + // A removal that found nothing keeps its own zero, so the banner can say so. + source.reportClear({ cleared: true, count: 0 }) + expect(source.hooks.toast.getSnapshot()).toEqual({ kind: 'cleared', count: 0, seq: 2 }) + source.reportClear({ cleared: false }) + expect(source.hooks.toast.getSnapshot()).toEqual({ kind: 'clearFailed', seq: 3 }) + }) }) describe('ScheduleDeleteToast', () => { @@ -68,4 +79,25 @@ describe('ScheduleDeleteToast', () => { mount({ kind: 'deleteFailed', seq: 2 }, zh) expect(screen.getByRole('alert').textContent).toBe(zh['toast.deleteFailed']) }) + + it('states how many inactive tasks a bulk removal cleared, singular for one', () => { + vi.useFakeTimers() + const many = mount({ kind: 'cleared', count: 4, seq: 3 }) + expect(screen.getByRole('alert').textContent).toBe(en['toast.cleared.other'].replace('{count}', '4')) + act(() => { vi.runAllTimers() }) + expect(many.dismiss).toHaveBeenCalledOnce() + cleanup() + mount({ kind: 'cleared', count: 1, seq: 4 }) + expect(screen.getByRole('alert').textContent).toBe(en['toast.cleared.one'].replace('{count}', '1')) + }) + + it('states that a bulk removal found nothing instead of a zero count', () => { + mount({ kind: 'cleared', count: 0, seq: 4 }, zh) + expect(screen.getByRole('alert').textContent).toBe(zh['toast.clearedNone']) + }) + + it('shows the warning banner for a bulk removal that failed', () => { + mount({ kind: 'clearFailed', seq: 5 }, zh) + expect(screen.getByRole('alert').textContent).toBe(zh['toast.clearFailed']) + }) }) diff --git a/packages/client/ui-schedule/tests/task-detail.client.spec.tsx b/packages/client/ui-schedule/tests/task-detail.client.spec.tsx index ff64d41b88..97bef63a33 100644 --- a/packages/client/ui-schedule/tests/task-detail.client.spec.tsx +++ b/packages/client/ui-schedule/tests/task-detail.client.spec.tsx @@ -122,6 +122,7 @@ function mount( onDelete: vi.fn(async () => 'deleted'), onRetry: vi.fn(async () => {}), onNewTask: vi.fn(), + onClearEnded: vi.fn(async () => {}), onUpdateTiming: vi.fn(async ({ expected }) => ({ ok: true, value: { id: expected.id, updated: false, record: expected }, })), diff --git a/packages/client/ui-schedule/tests/task-manager-page.client.spec.tsx b/packages/client/ui-schedule/tests/task-manager-page.client.spec.tsx index 822b08571b..e185f2d136 100644 --- a/packages/client/ui-schedule/tests/task-manager-page.client.spec.tsx +++ b/packages/client/ui-schedule/tests/task-manager-page.client.spec.tsx @@ -162,6 +162,7 @@ function mount( initial: Partial> = {}, dictionary: typeof en | typeof zh = en, overrides: Partial = {}, + options: { startAtAll?: boolean } = {}, ) { let snapshot: CatalogSnapshot = { records, status: 'ready', deleting: [], settled: true, readRequest: 0, readSettled: 0, ...initial, @@ -178,6 +179,7 @@ function mount( onDelete: vi.fn(async () => 'deleted'), onRetry: vi.fn(async () => {}), onNewTask: vi.fn(), + onClearEnded: vi.fn(async () => {}), onUpdateTiming: vi.fn(async ({ expected }) => ({ ok: true, value: { id: expected.id, updated: false, record: expected }, })), @@ -193,6 +195,11 @@ function mount( ...overrides, } const view = render() + // The page starts on Active; cases about the full list select All first. The + // default filter itself is covered by its own case below. + if (options.startAtAll !== false) { + fireEvent.click(screen.getByRole('button', { name: dictionary['statusFilter.all'] })) + } return { props, view, updateTiming: vi.mocked(props.onUpdateTiming), @@ -640,9 +647,11 @@ describe('Task manager catalog', () => { expect(within(filters).getAllByRole('group')).toHaveLength(1) const statuses = within(filters).getByRole('group', { name: en['statusFilter.label'] }) expect(statuses.parentElement).toBe(filters) - expect(within(statuses).getAllByRole('button')[0]?.textContent).toBe(en['statusFilter.all']) + expect(within(statuses).getAllByRole('button')[0]?.textContent).toBe(en['status.active']) // The mock's `.searchbox`: the same field carries the leading glyph, the // placeholder, and — once a value exists — the inline clear control. + const searchTools = page.querySelector(`.${css.searchTools!}`)! + expect(searchTools.contains(page.querySelector(`.${css.searchField!}`))).toBe(true) const field = page.querySelector(`.${css.searchField!}`)! const input = screen.getByRole('searchbox', { name: en['search.label'] }) expect(field.contains(input)).toBe(true) @@ -835,6 +844,18 @@ describe('Task manager catalog', () => { expect(taskNames()).toEqual(['Send summary', 'Review release', 'Check metrics']) }) + it.each([en, zh])('starts on the active status filter', (dictionary) => { + mount({ records: [ended, every, after] }, dictionary, {}, { startAtAll: false }) + const list = screen.getByRole('list', { name: dictionary['list.label'] }) + const names = within(list).queryAllByRole('button') + .filter(button => button.closest('li') !== null) + .map(button => button.getAttribute('aria-label')) + const statuses = screen.getByRole('group', { name: dictionary['statusFilter.label'] }) + expect(within(statuses).getByRole('button', { name: dictionary['status.active'] }).getAttribute('aria-pressed')).toBe('true') + expect(within(statuses).getByRole('button', { name: dictionary['statusFilter.all'] }).getAttribute('aria-pressed')).toBe('false') + expect(names).toEqual(['Send summary', 'Check metrics']) + }) + it.each([en, zh])('combines all, active, and inactive status filters with search', (dictionary) => { mount({ records: [ended, every, after] }, dictionary) const list = screen.getByRole('list', { name: dictionary['list.label'] }) @@ -942,6 +963,209 @@ describe('Task manager catalog', () => { }) }) +describe('clearing inactive tasks', () => { + // A distinct identity: the module's `ended` case reuses the active row's id. + const endedTask: ScheduleCatalogEntry = { + ...ended, id: 'task-ended' as ScheduleId, title: 'Ended summary', prompt: 'Ended summary', + } + const anotherEnded: ScheduleCatalogEntry = { + ...ended, id: 'task-ended-two' as ScheduleId, title: 'Ended report', prompt: 'Ended report', + } + + /** The heading's actions; only the primary New control remains there. */ + function creationActions(): HTMLElement { + return document.querySelector(`.${css.creationActions!}`)! + } + + /** The search row's 36px More trigger. */ + function moreTrigger(dictionary: typeof en | typeof zh = en): HTMLElement { + return screen.getByRole('button', { name: dictionary['list.more'] }) + } + + /** + * Open the cleanup confirmation through the More menu. + * @param dictionary - locale under test. + * @param count - ended rows the page's complete catalog holds. + * @returns the confirmation dialog. + */ + function openClear(dictionary: typeof en | typeof zh = en, count = 1): HTMLElement { + fireEvent.click(moreTrigger(dictionary)) + fireEvent.click(screen.getByRole('menuitem', { name: dictionary['clear.action'] })) + return screen.getByRole('dialog', { + name: dictionary['clear.title'].replace('{count}', String(count)), + }) + } + + it('offers the cleanup in the search row only while an ended task is visible', () => { + const h = mount() + expect(creationActions().textContent).toBe(en['new.action']) + expect(screen.queryByRole('button', { name: en['list.more'] })).toBeNull() + h.update({ records: [...records, endedTask] }) + // The cleanup is the search row's More menu, not a second heading action. + expect(document.querySelector(`.${css.searchTools!}`)?.contains(moreTrigger())).toBe(true) + expect(within(creationActions()).getAllByRole('button')).toHaveLength(1) + // The Active view hides the cleanup even though the catalog holds ended rows. + fireEvent.click(screen.getByRole('button', { name: en['status.active'] })) + expect(screen.queryByRole('button', { name: en['list.more'] })).toBeNull() + fireEvent.click(screen.getByRole('button', { name: en['statusFilter.all'] })) + expect(moreTrigger()).toBeDefined() + }) + + it('states the complete ended set in the confirmation, not the filtered rows', () => { + mount({ records: [...records, endedTask, anotherEnded] }) + fireEvent.change(screen.getByRole('searchbox', { name: en['search.label'] }), { + target: { value: 'Send summary' }, + }) + expect(taskNames()).toEqual(['Send summary']) + const dialog = openClear(en, 2) + expect(within(dialog).getByText(en['clear.description'])).toBeDefined() + }) + + it('closes the cleanup menu without opening the confirmation', () => { + mount({ records: [at, endedTask] }) + fireEvent.click(moreTrigger()) + expect(screen.getByRole('menuitem', { name: en['clear.action'] })).toBeDefined() + fireEvent.keyDown(document, { key: 'Escape' }) + expect(screen.queryByRole('menuitem', { name: en['clear.action'] })).toBeNull() + expect(screen.queryByRole('dialog')).toBeNull() + }) + + it('removes nothing when the confirmation is dismissed', () => { + const h = mount({ records: [at, endedTask] }) + openClear() + fireEvent.click(screen.getByRole('button', { name: en['delete.cancel'] })) + expect(screen.queryByRole('dialog')).toBeNull() + expect(h.props.onClearEnded).not.toHaveBeenCalled() + }) + + it('keeps the confirmation open until the settled removal, then returns focus', async () => { + const pending = Promise.withResolvers() + const onClearEnded = vi.fn(() => pending.promise) + mount({ records: [at, endedTask] }, en, { onClearEnded }) + const trigger = moreTrigger() + trigger.focus() + const dialog = openClear() + const confirm = within(dialog).getByRole('button', { name: en['clear.confirm'] }) + confirm.focus() + fireEvent.click(confirm) + expect(onClearEnded).toHaveBeenCalledOnce() + + const running = screen.getByRole('dialog') + const runningButton = within(running).getByRole('button', { name: en['clear.pending'] }) + // The pressed control keeps the focus and reads as busy; disabling it would + // hand focus to the document body mid-removal. + expect(runningButton.getAttribute('aria-disabled')).toBe('true') + expect(document.activeElement).toBe(runningButton) + expect(within(running).getByRole('button', { name: en['delete.cancel'] }).hasAttribute('disabled')).toBe(true) + // A second press while the removal runs must not start another request. + fireEvent.click(runningButton) + expect(onClearEnded).toHaveBeenCalledOnce() + // The confirmation is the only place that states the removal still runs, so + // the Escape that would close it waits for the outcome. + fireEvent.keyDown(running, { key: 'Escape' }) + expect(screen.getByRole('dialog')).toBe(running) + + await act(async () => { pending.resolve(undefined) }) + expect(screen.queryByRole('dialog')).toBeNull() + expect(document.activeElement).toBe(trigger) + }) + + it('falls back to the page heading when the settled cleanup hides its trigger', async () => { + const pending = Promise.withResolvers() + const onClearEnded = vi.fn(() => pending.promise) + const h = mount({ records: [at, endedTask] }, en, { onClearEnded }) + const trigger = moreTrigger() + trigger.focus() + const dialog = openClear() + fireEvent.click(within(dialog).getByRole('button', { name: en['clear.confirm'] })) + // The acknowledged removal's readback drops the ended rows, and with them the + // trigger the Modal captured when it opened. + act(() => { h.update({ records: [at] }) }) + expect(screen.queryByRole('button', { name: en['list.more'] })).toBeNull() + await act(async () => { pending.resolve(undefined) }) + expect(screen.queryByRole('dialog')).toBeNull() + expect(document.activeElement).toBe(screen.getByRole('heading', { level: 1 })) + }) + + it('keeps focus in the confirmation when the selected task disappears behind it', () => { + const h = mount({ records: [at, endedTask] }) + fireEvent.click(screen.getByRole('button', { name: 'Ended summary' })) + expect(screen.getByRole('complementary', { name: en['detail.label'] })).toBeDefined() + const dialog = openClear() + // A refresh drops the selected task while the confirmation is open; closing + // the detail must not take the focus out of the dialog. + act(() => { h.update({ records: [at] }) }) + expect(screen.queryByRole('complementary', { name: en['detail.label'] })).toBeNull() + expect(dialog.contains(document.activeElement)).toBe(true) + fireEvent.keyDown(dialog, { key: 'Escape' }) + expect(screen.queryByRole('dialog')).toBeNull() + // The row that held the focus is gone with the detail, so the heading takes it. + expect(document.activeElement).toBe(screen.getByRole('heading', { level: 1 })) + }) + + it('falls back to the page heading when another cleanup removes the trigger', () => { + const h = mount({ records: [at, endedTask] }) + const trigger = moreTrigger() + trigger.focus() + const dialog = openClear() + // Another client's cleanup removes the ended rows while this confirmation + // is open, so the control the Modal captured is unmounted too. + act(() => { h.update({ records: [at] }) }) + expect(screen.queryByRole('button', { name: en['list.more'] })).toBeNull() + fireEvent.keyDown(dialog, { key: 'Escape' }) + expect(screen.queryByRole('dialog')).toBeNull() + expect(document.activeElement).toBe(screen.getByRole('heading', { level: 1 })) + }) + + it('focuses the rebuilt trigger when the dialog closes', () => { + const h = mount({ records: [at, endedTask] }) + const dialog = openClear() + // Another client clears the ended rows, then a delivery ends one more while + // the confirmation stays open: the trigger unmounts and a new one mounts. + act(() => { h.update({ records: [at] }) }) + act(() => { h.update({ records: [at, anotherEnded] }) }) + const rebuilt = moreTrigger() + fireEvent.keyDown(dialog, { key: 'Escape' }) + expect(screen.queryByRole('dialog')).toBeNull() + expect(document.activeElement).toBe(rebuilt) + }) + + it('moves focus to the heading when a late readback removes the trigger', async () => { + const pending = Promise.withResolvers() + const onClearEnded = vi.fn(() => pending.promise) + const h = mount({ records: [at, endedTask] }, en, { onClearEnded }) + const trigger = moreTrigger() + trigger.focus() + const dialog = openClear() + fireEvent.click(within(dialog).getByRole('button', { name: en['clear.confirm'] })) + // The removal settles while the catalog still lists the ended row: the + // dialog closes and focus returns to the trigger. + await act(async () => { pending.resolve(undefined) }) + expect(document.activeElement).toBe(trigger) + // A readback superseded by `schedule/changed` publishes afterwards and takes + // the trigger with its row; focus must not stay on the document body. + act(() => { h.update({ records: [at] }) }) + expect(document.activeElement).toBe(screen.getByRole('heading', { level: 1 })) + }) + + it('closes only the confirmation on Escape while a task detail is open', () => { + mount({ records: [at, endedTask] }) + fireEvent.click(screen.getByRole('button', { name: 'Review release' })) + const detail = screen.getByRole('complementary', { name: en['detail.label'] }) + const dialog = openClear() + fireEvent.keyDown(dialog, { key: 'Escape' }) + expect(screen.queryByRole('dialog')).toBeNull() + expect(screen.getByRole('complementary', { name: en['detail.label'] })).toBe(detail) + }) + + it('states the cleanup and its confirmation in Chinese', () => { + mount({ records: [at, endedTask] }, zh) + const dialog = openClear(zh, 1) + expect(within(dialog).getByText(zh['clear.description'])).toBeDefined() + expect(within(dialog).getByRole('button', { name: zh['clear.confirm'] })).toBeDefined() + }) +}) + describe.each([['English', en], ['Chinese', zh]] as const)('task detail views in %s', (_name, dictionary) => { it('opens Rules under the selected task and separates rule fields from saved deliveries', async () => { const labels = detailViewLabels(dictionary) @@ -1551,7 +1775,7 @@ describe('Task manager details', () => { expect(screen.getByRole('heading', { level: 1, name: '自动化任务' })).toBeDefined() expect(screen.getByRole('searchbox', { name: '搜索任务' })).toBeDefined() const statuses = screen.getByRole('group', { name: '任务状态' }) - expect(within(statuses).getAllByRole('button')[0]?.textContent).toBe('全部') + expect(within(statuses).getAllByRole('button')[0]?.textContent).toBe('已开启') fireEvent.click(screen.getByRole('button', { name: 'Check metrics' })) const detail = screen.getByRole('complementary', { name: '任务详情' }) expect(within(detail).getByText(zh['rule.everySeconds'])).toBeDefined() @@ -3814,11 +4038,12 @@ describe('Mock detail metrics shared with the task list', () => { ) // The Plugins toolbar's Add-plugin capsule, repeated by the creation action. expect(rule('creationActions')).toMatch(/gap:\s*16px;/) - const newButton = rule('newButton') - expect(newButton).toMatch(/height:\s*32px;/) - expect(newButton).toMatch(/font-size:\s*13px;/) - expect(newButton).toMatch(/padding:\s*0 12px;/) - expect(newButton).toMatch(/border-radius:\s*16px;/) + // The New action and the bulk cleanup share this capsule geometry. + const headerAction = rule('headerAction') + expect(headerAction).toMatch(/height:\s*32px;/) + expect(headerAction).toMatch(/font-size:\s*13px;/) + expect(headerAction).toMatch(/padding:\s*0 12px;/) + expect(headerAction).toMatch(/border-radius:\s*16px;/) const headingTitle = /\.pageHeading h1\s*\{([^}]*)\}/.exec(stylesheet)?.[1] ?? '' // The first-level title step is the Plugins page's `pageTitle`, not the // mock's larger page heading; the equality case below pins that pair. @@ -3834,9 +4059,14 @@ describe('Mock detail metrics shared with the task list', () => { expect(tab).toMatch(/font-size:\s*14px;/) // The selected chip keeps the chip's own weight; only the fill marks it. expect(rule('filterTabActive')).not.toMatch(/font-weight/) - // The mock's `.searchbox` keeps its hairline at the repository's 0.5px. + // The search row fixes the mock's 36px control height for the field and the + // square More trigger, and keeps the hairline at the repository's 0.5px. + expect(rule('searchTools')).toMatch(/--task-search-height:\s*36px;/) + expect(rule('catalogMore')).toMatch(/width:\s*var\(--task-search-height\);/) + expect(rule('catalogMore')).toMatch(/height:\s*var\(--task-search-height\);/) + expect(rule('catalogMore')).toMatch(/padding:\s*0;/) const field = rule('searchField') - expect(field).toMatch(/height:\s*36px;/) + expect(field).toMatch(/height:\s*var\(--task-search-height\);/) expect(field).toMatch(/padding:\s*0 10px;/) expect(field).toMatch(/border:\s*0\.5px solid var\(--dsw-alias-border-l3\);/) expect(field).toMatch(/border-radius:\s*12px;/) diff --git a/packages/client/ui-secret-mask/locale/de.json b/packages/client/ui-secret-mask/locale/de.json new file mode 100644 index 0000000000..e75c2392f5 --- /dev/null +++ b/packages/client/ui-secret-mask/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Maskierung von Anmeldeinformationen", + "description": "Maskiert erkannte Schlüssel, Token und Passwörter in gesendeten Nachrichten." + } +} diff --git a/packages/client/ui-secret-mask/locale/es.json b/packages/client/ui-secret-mask/locale/es.json new file mode 100644 index 0000000000..91672e7f75 --- /dev/null +++ b/packages/client/ui-secret-mask/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Enmascaramiento de credenciales", + "description": "Enmascara claves, tokens y contraseñas reconocidas en los mensajes enviados." + } +} diff --git a/packages/client/ui-secret-mask/locale/fr.json b/packages/client/ui-secret-mask/locale/fr.json new file mode 100644 index 0000000000..f174fb8bd1 --- /dev/null +++ b/packages/client/ui-secret-mask/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Masquage des informations d'identification", + "description": "Masque les clés, jetons et mots de passe reconnus dans les messages envoyés." + } +} diff --git a/packages/client/ui-secret-mask/locale/pt.json b/packages/client/ui-secret-mask/locale/pt.json new file mode 100644 index 0000000000..4f6910096c --- /dev/null +++ b/packages/client/ui-secret-mask/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Mascaramento de credenciais", + "description": "Mascara chaves, tokens e senhas reconhecidas em mensagens enviadas." + } +} diff --git a/packages/client/ui-secret-mask/locale/ru.json b/packages/client/ui-secret-mask/locale/ru.json new file mode 100644 index 0000000000..0f9d26af72 --- /dev/null +++ b/packages/client/ui-secret-mask/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Маскирование учетных данных", + "description": "Маскирует распознанные ключи, токены и пароли в отправленных сообщениях." + } +} diff --git a/packages/client/ui-secret-mask/src/client/index.ts b/packages/client/ui-secret-mask/src/client/index.ts index 4346d37bf2..91f02986f6 100644 --- a/packages/client/ui-secret-mask/src/client/index.ts +++ b/packages/client/ui-secret-mask/src/client/index.ts @@ -15,7 +15,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' import { SecretText } from './SecretText.tsx' import { createSecretRule } from './secret-rule.ts' export type {} from './secret-rule.ts' -import { en, ja, NS, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh } from './locales.ts' /** Required services: the copy, the node renderer registry, and the UserText rule registry. */ export const inject = ['locale', 'slots', 'uiConversation'] @@ -25,7 +25,7 @@ export const inject = ['locale', 'slots', 'uiConversation'] * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-secret-mask: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-secret-mask: dictionaries') ctx.uiConversation.userText.register(createSecretRule()) ctx.slots.inject('conversation.user.text.node', () => ctx.slots.register({ name: 'conversation.user.text.node', key: 'secret', priority: 100, locale: NS, diff --git a/packages/client/ui-secret-mask/src/client/locales.ts b/packages/client/ui-secret-mask/src/client/locales.ts index 7bf3c0915b..2034471fe6 100644 --- a/packages/client/ui-secret-mask/src/client/locales.ts +++ b/packages/client/ui-secret-mask/src/client/locales.ts @@ -32,3 +32,33 @@ export const ja = { 'secret.reveal': 'シークレットを表示', 'secret.hide': 'シークレットを隠す', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'secret.reveal': 'Mostrar secreto', + 'secret.hide': 'Ocultar secreto', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'secret.reveal': 'Afficher le secret', + 'secret.hide': 'Cacher le secret', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'secret.reveal': 'Geheimnis zeigen', + 'secret.hide': 'Geheimnis verbergen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'secret.reveal': 'Показать секрет', + 'secret.hide': 'Скрыть секрет', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'secret.reveal': 'Mostrar segredo', + 'secret.hide': 'Ocultar segredo', +} satisfies Record diff --git a/packages/client/ui-session/locale/de.json b/packages/client/ui-session/locale/de.json new file mode 100644 index 0000000000..cae002c97b --- /dev/null +++ b/packages/client/ui-session/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Verbindung zur Sitzungsschnittstelle", + "description": "Verbinden Sie Sitzungssteuerung und Status mit React-Schnittstellenkomponenten." + } +} diff --git a/packages/client/ui-session/locale/es.json b/packages/client/ui-session/locale/es.json new file mode 100644 index 0000000000..0febc1d178 --- /dev/null +++ b/packages/client/ui-session/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Conexión de interfaz de sesión", + "description": "Conecte el control y el estado de la sesión a los componentes de la interfaz de React." + } +} diff --git a/packages/client/ui-session/locale/fr.json b/packages/client/ui-session/locale/fr.json new file mode 100644 index 0000000000..e4622ab45d --- /dev/null +++ b/packages/client/ui-session/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Connexion à l'interface de session", + "description": "Connectez le contrôle de session et l’état aux composants de l’interface React." + } +} diff --git a/packages/client/ui-session/locale/pt.json b/packages/client/ui-session/locale/pt.json new file mode 100644 index 0000000000..c5a9625131 --- /dev/null +++ b/packages/client/ui-session/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Conexão de interface de sessão", + "description": "Conecte o controle e o estado da sessão aos componentes da interface React." + } +} diff --git a/packages/client/ui-session/locale/ru.json b/packages/client/ui-session/locale/ru.json new file mode 100644 index 0000000000..c4df84e0a3 --- /dev/null +++ b/packages/client/ui-session/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Подключение интерфейса сеанса", + "description": "Подключите управление сеансом и состояние к компонентам интерфейса React." + } +} diff --git a/packages/client/ui-settings-account/locale/de.json b/packages/client/ui-settings-account/locale/de.json new file mode 100644 index 0000000000..1f7c9a6c99 --- /dev/null +++ b/packages/client/ui-settings-account/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Benutzeroberfläche für Kontoeinstellungen", + "description": "Melden Sie sich an, um DeepSeek zu verwalten und die Abrechnungsseite der Plattform zu öffnen." + } +} diff --git a/packages/client/ui-settings-account/locale/es.json b/packages/client/ui-settings-account/locale/es.json new file mode 100644 index 0000000000..de8904f3d3 --- /dev/null +++ b/packages/client/ui-settings-account/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de configuración de cuenta", + "description": "Inicie sesión para administrar DeepSeek y abra la página de facturación de la plataforma." + } +} diff --git a/packages/client/ui-settings-account/locale/fr.json b/packages/client/ui-settings-account/locale/fr.json new file mode 100644 index 0000000000..6b64e79108 --- /dev/null +++ b/packages/client/ui-settings-account/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de configuration du compte", + "description": "Connectez-vous pour gérer DeepSeek et ouvrez la page de facturation de la plateforme." + } +} diff --git a/packages/client/ui-settings-account/locale/pt.json b/packages/client/ui-settings-account/locale/pt.json new file mode 100644 index 0000000000..8a4619488e --- /dev/null +++ b/packages/client/ui-settings-account/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de configuração de conta", + "description": "Faça login para gerenciar DeepSeek e abra a página de faturamento da plataforma." + } +} diff --git a/packages/client/ui-settings-account/locale/ru.json b/packages/client/ui-settings-account/locale/ru.json new file mode 100644 index 0000000000..a1b2fddd9b --- /dev/null +++ b/packages/client/ui-settings-account/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс настройки учетной записи", + "description": "Войдите в систему для управления DeepSeek и откройте страницу выставления счетов платформы." + } +} diff --git a/packages/client/ui-settings-account/src/client/index.ts b/packages/client/ui-settings-account/src/client/index.ts index b70e5097a1..45f13a1b97 100644 --- a/packages/client/ui-settings-account/src/client/index.ts +++ b/packages/client/ui-settings-account/src/client/index.ts @@ -24,7 +24,7 @@ import { createPlatformPages, type PlatformPages } from './platform-pages.ts' import { AccountSection, type AccountSnapshot, type AccountSectionInjected } from './AccountSection.tsx' import { createBonusNoticeController } from './bonus-notices.ts' import { accountClientMetadata } from './client-metadata.ts' -import { en, ja, zh, type AccountKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type AccountKey } from './locales.ts' import { AccountQuotaNotice, type AccountQuotaNoticeInjected } from './AccountQuotaNotice.tsx' import { DESKTOP_ONBOARDING_NAMESPACE, type OnboardingSettings } from '../onboarding-settings.ts' import { DesktopOnboardingController } from './onboarding-state.ts' @@ -46,7 +46,7 @@ export const inject = ['slots', 'locale', 'remote', 'remote.account', 'remote.se /** Register account UI in Desktop and plain Web clients. @param ctx - client plugin context. */ export function apply(ctx: Context): void { const desktop = 'dshDesktop' in globalThis - ctx.effect(() => ctx.locale.register('settings.account', { en, zh, ja }), 'account: dictionaries') + ctx.effect(() => ctx.locale.register('settings.account', { en, zh, ja, es, fr, de, ru, pt }), 'account: dictionaries') const t = ctx.locale.bind('settings.account') const page = globalThis as Partial> const config = ContactConfig(page[CONTACT_CONFIG_GLOBAL] ?? {}) diff --git a/packages/client/ui-settings-account/src/client/locales.ts b/packages/client/ui-settings-account/src/client/locales.ts index 5244c0743f..43ed4d17c1 100644 --- a/packages/client/ui-settings-account/src/client/locales.ts +++ b/packages/client/ui-settings-account/src/client/locales.ts @@ -1,5 +1,5 @@ /** Account settings copy, owned by the account feature. */ -import { onboardingZh, onboardingEn, onboardingJa } from './locales/onboarding.ts' +import { onboardingZh, onboardingEn, onboardingJa, onboardingEs, onboardingFr, onboardingDe, onboardingRu, onboardingPt } from './locales/onboarding.ts' /** English account dictionary. */ export const en = { @@ -131,3 +131,178 @@ export const ja = { 'quotaTopUp': 'チャージ', 'bonusNoticeTitle': 'ボーナスが付与されました', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + modelSignInRequired: 'Modelo no disponible. Por favor inicia sesión y vuelve a intentarlo.', + sessionExpired: 'Has cerrado sesión en tu cuenta, inicia sesión nuevamente.', + ...onboardingEs, + close: 'Cerrar', addApiKey: 'Agregar clave API', retry: 'Iniciar sesión nuevamente', + loginTitle: 'empezar', loginDescription: 'Inicie sesión en su cuenta DeepSeek o agregue una clave API para comenzar. Sus proyectos y archivos se almacenan localmente.', + browserTitle: 'Esperando iniciar sesión', browserPrompt: '¿La página no se abrió automáticamente? ', copyLink: 'Copiar enlace de inicio de sesión', copiedLink: 'Enlace copiado', copyFailed: 'Copia fallida', + browserDescription: ', luego ábrelo en tu navegador para terminar de iniciar sesión.', + timeoutTitle: 'Se agotó el tiempo de inicio de sesión', timeoutDescription: 'Inicia sesión nuevamente para continuar.', + failureTitle: 'No se pudo iniciar sesión', + platformFailed: 'No se pudo completar la operación. Intentar otra vez.', platformRetry: 'Reintentar', + loading: 'Cargando…', backToHarness: 'Volver a DeepSeek Harness', + settings: 'Configuración', contactUs: 'Comentarios', menu: 'Menú de cuenta', + nav: 'Cuenta', signedIn: 'Iniciado sesión en DeepSeek', signedOut: 'No iniciado sesión', + signIn: 'Iniciar sesión', signOut: 'Cerrar sesión', + signOutUnknownDescription: 'No se pudieron verificar las tareas en ejecución. Cerrar sesión puede interrumpir las tareas que utilizan esta cuenta. ¿Cerrar sesión ahora?', + signOutDescription: 'Cerrar sesión no eliminará ningún dato. Puede iniciar sesión en esta cuenta nuevamente.', + signOutRunningDescription: 'Las tareas se están ejecutando actualmente. Cerrar sesión los interrumpirá. ¿Cerrar sesión ahora?', cancel: 'Cancelar', open: 'Abrir navegador', + initializing: 'Iniciando sesión...', waiting: 'Continuar en tu navegador', + completing: 'Completando el inicio de sesión…', expired: 'Iniciar sesión caducado. Intentar otra vez.', + failed: 'No se pudo completar la operación. Intentar otra vez.', + noResponse: 'Algo salió mal. Verifique su conexión de red e inténtelo nuevamente.', + settingsSignedOutTitle: 'No has iniciado sesión en DeepSeek Harness', + settingsSignedOutDescription: 'Inicie sesión en DeepSeek Harness para obtener su clave API dedicada', + signInDescription: 'Utilice su cuenta DeepSeek para comenzar.', + profileUnavailable: 'Los detalles de la cuenta aún no están disponibles.', + balance: 'Saldo recargado', bonusBalance: 'Saldo concedido', balanceUnavailable: 'Ver en plataforma', balanceSignedOut: 'Inicia sesión para ver', + accountInfo: 'Más información de la cuenta', more: 'Más', usage: 'Ver uso', topUp: 'Recargar', + quotaTitle: 'No hay saldo disponible', + quotaDescription: 'DeepSeek Harness no puede iniciar una nueva tarea con esta cuenta cuando no hay saldo disponible. ¿Quieres recargar? También puedes recargar más tarde en Configuración → Cuenta.', + quotaTopUp: 'Recargar', + bonusNoticeTitle: 'Bono acreditado', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + modelSignInRequired: 'Modèle indisponible. Veuillez vous connecter et réessayer.', + sessionExpired: 'Vous vous êtes déconnecté de votre compte, veuillez vous reconnecter.', + ...onboardingFr, + close: 'Fermer', addApiKey: 'Ajouter une clé API', retry: 'Connectez-vous à nouveau', + loginTitle: 'Commencez', loginDescription: 'Connectez-vous à votre compte DeepSeek ou ajoutez une clé API pour commencer. Vos projets et fichiers sont stockés localement.', + browserTitle: 'En attente de connexion', browserPrompt: "La page ne s'est pas ouverte automatiquement ? ", copyLink: 'Copier le lien de connexion', copiedLink: 'Lien copié', copyFailed: 'Échec de la copie', + browserDescription: ', puis ouvrez-le dans votre navigateur pour terminer la connexion.', + timeoutTitle: 'La connexion a expiré', timeoutDescription: 'Connectez-vous à nouveau pour continuer.', + failureTitle: 'Impossible de se connecter', + platformFailed: "Impossible de terminer l'opération. Essayer à nouveau.", platformRetry: 'Réessayer', + loading: 'Chargement…', backToHarness: 'Retour à DeepSeek Harness', + settings: 'Paramètres', contactUs: 'Commentaires', menu: 'Menu du compte', + nav: 'Compte', signedIn: 'Connecté à DeepSeek', signedOut: 'Non connecté', + signIn: 'Connectez-vous', signOut: 'Se déconnecter', + signOutUnknownDescription: "Impossible de vérifier les tâches en cours d'exécution. La déconnexion peut interrompre les tâches utilisant ce compte. Se déconnecter maintenant ?", + signOutDescription: 'La déconnexion ne supprimera aucune donnée. Vous pouvez vous reconnecter à ce compte.', + signOutRunningDescription: "Les tâches sont actuellement en cours d'exécution. La déconnexion les interrompra. Se déconnecter maintenant ?", cancel: 'Annuler', open: 'Ouvrir le navigateur', + initializing: 'Début de la connexion…', waiting: 'Continuez dans votre navigateur', + completing: 'Connexion terminée…', expired: 'Connexion expirée. Essayer à nouveau.', + failed: "Impossible de terminer l'opération. Essayer à nouveau.", + noResponse: "Quelque chose s'est mal passé. Veuillez vérifier votre connexion réseau et réessayer.", + settingsSignedOutTitle: "Vous n'êtes pas connecté à DeepSeek Harness", + settingsSignedOutDescription: 'Connectez-vous à DeepSeek Harness pour obtenir votre clé API dédiée', + signInDescription: 'Utilisez votre compte DeepSeek pour commencer.', + profileUnavailable: 'Les détails du compte ne sont pas encore disponibles.', + balance: 'Solde rechargé', bonusBalance: 'Solde accordé', balanceUnavailable: 'Afficher sur la plateforme', balanceSignedOut: 'Connectez-vous pour voir', + accountInfo: "Plus d'informations sur le compte", more: 'Plus', usage: "Afficher l'utilisation", topUp: 'Recharger', + quotaTitle: 'Aucun solde disponible', + quotaDescription: "DeepSeek Harness ne peut pas démarrer une nouvelle tâche avec ce compte lorsqu'aucun solde n'est disponible. Souhaitez-vous recharger ? Vous pouvez également recharger plus tard dans Paramètres → Compte.", + quotaTopUp: 'Recharger', + bonusNoticeTitle: 'Bonus crédité', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + modelSignInRequired: 'Modell nicht verfügbar. Bitte melden Sie sich an und versuchen Sie es erneut.', + sessionExpired: 'Sie haben sich von Ihrem Konto abgemeldet. Bitte melden Sie sich erneut an.', + ...onboardingDe, + close: 'Schließen', addApiKey: 'Fügen Sie den API-Schlüssel hinzu', retry: 'Melden Sie sich erneut an', + loginTitle: 'Fangen Sie an', loginDescription: 'Melden Sie sich bei Ihrem DeepSeek-Konto an oder fügen Sie einen API-Schlüssel hinzu, um loszulegen. Ihre Projekte und Dateien werden lokal gespeichert.', + browserTitle: 'Warten auf die Anmeldung', browserPrompt: 'Seite wurde nicht automatisch geöffnet? ', copyLink: 'Anmeldelink kopieren', copiedLink: 'Link kopiert', copyFailed: 'Das Kopieren ist fehlgeschlagen', + browserDescription: ', und öffnen Sie es dann in Ihrem Browser, um die Anmeldung abzuschließen.', + timeoutTitle: 'Zeitüberschreitung bei der Anmeldung', timeoutDescription: 'Melden Sie sich erneut an, um fortzufahren.', + failureTitle: 'Konnte mich nicht anmelden', + platformFailed: 'Der Vorgang konnte nicht abgeschlossen werden. Versuchen Sie es erneut.', platformRetry: 'Erneut versuchen', + loading: 'Laden…', backToHarness: 'Zurück zu DeepSeek Harness', + settings: 'Einstellungen', contactUs: 'Rückmeldung', menu: 'Kontomenü', + nav: 'Konto', signedIn: 'Angemeldet bei DeepSeek', signedOut: 'Nicht angemeldet', + signIn: 'Melden Sie sich an', signOut: 'Abmelden', + signOutUnknownDescription: 'Laufende Aufgaben konnten nicht überprüft werden. Durch das Abmelden werden möglicherweise Aufgaben mit diesem Konto unterbrochen. Jetzt abmelden?', + signOutDescription: 'Durch das Abmelden werden keine Daten gelöscht. Sie können sich erneut bei diesem Konto anmelden.', + signOutRunningDescription: 'Derzeit werden Aufgaben ausgeführt. Durch das Abmelden werden sie unterbrochen. Jetzt abmelden?', cancel: 'Abbrechen', open: 'Browser öffnen', + initializing: 'Anmeldung beginnt…', waiting: 'Fahren Sie in Ihrem Browser fort', + completing: 'Anmeldung wird abgeschlossen…', expired: 'Anmeldung abgelaufen. Versuchen Sie es erneut.', + failed: 'Der Vorgang konnte nicht abgeschlossen werden. Versuchen Sie es erneut.', + noResponse: 'Etwas ist schief gelaufen. Bitte überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es erneut.', + settingsSignedOutTitle: 'Sie sind nicht bei DeepSeek Harness angemeldet', + settingsSignedOutDescription: 'Melden Sie sich bei DeepSeek Harness an, um Ihren dedizierten API-Schlüssel zu erhalten', + signInDescription: 'Verwenden Sie Ihr DeepSeek-Konto, um loszulegen.', + profileUnavailable: 'Kontodaten sind noch nicht verfügbar.', + balance: 'Aufgeladenes Guthaben', bonusBalance: 'Gewährte Balance', balanceUnavailable: 'Auf der Plattform ansehen', balanceSignedOut: 'Melden Sie sich an, um es anzuzeigen', + accountInfo: 'Weitere Kontoinformationen', more: 'Mehr', usage: 'Nutzung anzeigen', topUp: 'Auffüllen', + quotaTitle: 'Kein Guthaben verfügbar', + quotaDescription: 'DeepSeek Harness kann mit diesem Konto keine neue Aufgabe starten, wenn kein Guthaben verfügbar ist. Möchten Sie aufladen? Sie können das Guthaben auch später unter Einstellungen → Konto aufladen.', + quotaTopUp: 'Auffüllen', + bonusNoticeTitle: 'Bonus gutgeschrieben', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + modelSignInRequired: 'Модель недоступна. Пожалуйста, войдите в систему и повторите попытку.', + sessionExpired: 'Вы вышли из своей учетной записи, пожалуйста, войдите снова.', + ...onboardingRu, + close: 'Закрыть', addApiKey: 'Добавить ключ API', retry: 'Войдите снова', + loginTitle: 'Начать', loginDescription: 'Войдите в свою учетную запись DeepSeek или добавьте ключ API, чтобы начать. Ваши проекты и файлы хранятся локально.', + browserTitle: 'Ожидание входа в систему', browserPrompt: 'Страница не открылась автоматически? ', copyLink: 'Скопировать ссылку для входа', copiedLink: 'Ссылка скопирована', copyFailed: 'Не удалось скопировать', + browserDescription: ', затем откройте его в браузере, чтобы завершить вход.', + timeoutTitle: 'Время входа в систему истекло', timeoutDescription: 'Войдите еще раз, чтобы продолжить.', + failureTitle: 'Не удалось войти в систему', + platformFailed: 'Не удалось завершить операцию. Попробуйте еще раз.', platformRetry: 'Повторить', + loading: 'Загрузка…', backToHarness: 'Вернуться к DeepSeek Harness', + settings: 'Настройки', contactUs: 'Обратная связь', menu: 'Меню аккаунта', + nav: 'Аккаунт', signedIn: 'Вошёл в DeepSeek', signedOut: 'Не авторизован', + signIn: 'Войти', signOut: 'Выйти', + signOutUnknownDescription: 'Не удалось проверить запущенные задачи. Выход из системы может прервать выполнение задач с использованием этой учетной записи. Выйти сейчас?', + signOutDescription: 'Выход из системы не приведет к удалению данных. Вы можете снова войти в эту учетную запись.', + signOutRunningDescription: 'Задачи в настоящее время выполняются. Выход из системы прервет их. Выйти сейчас?', cancel: 'Отмена', open: 'Открыть браузер', + initializing: 'Начинаем вход…', waiting: 'Продолжить в браузере', + completing: 'Завершение входа…', expired: 'Срок действия входа истек. Попробуйте еще раз.', + failed: 'Не удалось завершить операцию. Попробуйте еще раз.', + noResponse: 'Что-то пошло не так. Пожалуйста, проверьте сетевое соединение и повторите попытку.', + settingsSignedOutTitle: 'Вы не вошли в DeepSeek Harness.', + settingsSignedOutDescription: 'Войдите в DeepSeek Harness, чтобы получить специальный ключ API.', + signInDescription: 'Используйте свою учетную запись DeepSeek, чтобы начать.', + profileUnavailable: 'Детали аккаунта пока недоступны.', + balance: 'Пополненный баланс', bonusBalance: 'Предоставленный баланс', balanceUnavailable: 'Посмотреть на платформе', balanceSignedOut: 'Войдите, чтобы просмотреть', + accountInfo: 'Дополнительная информация об аккаунте', more: 'Ещё', usage: 'Посмотреть использование', topUp: 'Пополнить', + quotaTitle: 'Нет доступного баланса', + quotaDescription: 'DeepSeek Harness не может запустить новую задачу с этой учетной записью, если на ней нет баланса. Хотите пополнить счет? Вы также можете пополнить счет позже в «Настройки» → «Аккаунт».', + quotaTopUp: 'Пополнить', + bonusNoticeTitle: 'Бонус начислен', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + modelSignInRequired: 'Modelo indisponível. Faça login e tente novamente.', + sessionExpired: 'Você saiu da sua conta, faça login novamente.', + ...onboardingPt, + close: 'Fechar', addApiKey: 'Adicionar chave API', retry: 'Faça login novamente', + loginTitle: 'Comece', loginDescription: 'Faça login em sua conta DeepSeek ou adicione uma chave API para começar. Seus projetos e arquivos são armazenados localmente.', + browserTitle: 'Aguardando login', browserPrompt: 'A página não abriu automaticamente? ', copyLink: 'Copiar link de login', copiedLink: 'Link copiado', copyFailed: 'Falha na cópia', + browserDescription: 'e abra-o no navegador para concluir o login.', + timeoutTitle: 'O tempo limite de login expirou', timeoutDescription: 'Faça login novamente para continuar.', + failureTitle: 'Não foi possível fazer login', + platformFailed: 'Não foi possível concluir a operação. Tente novamente.', platformRetry: 'Tentar novamente', + loading: 'Carregando…', backToHarness: 'Voltar para DeepSeek Harness', + settings: 'Configurações', contactUs: 'Comentários', menu: 'Menu da conta', + nav: 'Conta', signedIn: 'Conectado em DeepSeek', signedOut: 'Não conectado', + signIn: 'Faça login', signOut: 'Sair', + signOutUnknownDescription: 'Não foi possível verificar as tarefas em execução. Sair pode interromper tarefas usando esta conta. Sair agora?', + signOutDescription: 'Sair não excluirá nenhum dado. Você pode fazer login nesta conta novamente.', + signOutRunningDescription: 'As tarefas estão em execução no momento. Sair irá interrompê-los. Sair agora?', cancel: 'Cancelar', open: 'Abra o navegador', + initializing: 'Iniciando login…', waiting: 'Continue no seu navegador', + completing: 'Concluindo login…', expired: 'O login expirou. Tente novamente.', + failed: 'Não foi possível concluir a operação. Tente novamente.', + noResponse: 'Algo deu errado. Verifique sua conexão de rede e tente novamente.', + settingsSignedOutTitle: 'Você não está conectado a DeepSeek Harness', + settingsSignedOutDescription: 'Faça login em DeepSeek Harness para obter sua chave API dedicada', + signInDescription: 'Use sua conta DeepSeek para começar.', + profileUnavailable: 'Os detalhes da conta ainda não estão disponíveis.', + balance: 'Saldo reabastecido', bonusBalance: 'Saldo concedido', balanceUnavailable: 'Ver na plataforma', balanceSignedOut: 'Faça login para ver', + accountInfo: 'Mais informações da conta', more: 'Mais', usage: 'Ver uso', topUp: 'Recarregar', + quotaTitle: 'Nenhum saldo disponível', + quotaDescription: 'DeepSeek Harness não pode iniciar uma nova tarefa com esta conta quando não houver saldo disponível. Você gostaria de recarregar? Você também pode recarregar mais tarde em Configurações → Conta.', + quotaTopUp: 'Recarregar', + bonusNoticeTitle: 'Bônus creditado', +} satisfies Record diff --git a/packages/client/ui-settings-account/src/client/locales/onboarding.ts b/packages/client/ui-settings-account/src/client/locales/onboarding.ts index 977837e933..2fbf063a28 100644 --- a/packages/client/ui-settings-account/src/client/locales/onboarding.ts +++ b/packages/client/ui-settings-account/src/client/locales/onboarding.ts @@ -126,3 +126,218 @@ export const onboardingJa = { 'onboardingRetry': '再試行', 'onboardingLoading': '設定を読み込んでいます…', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const onboardingEs = { + onboardingArtworkLocale: 'en', + onboardingWelcome: 'Bienvenido a', + onboardingBrand: 'DeepSeek Harness', + onboardingIntroduction: 'DeepSeek Harness funciona en una carpeta local y utiliza herramientas para leer y escribir archivos en su computadora. Puede ayudarle a investigar y organizar información, crear documentos y hojas de cálculo, escribir código, solucionar problemas y más.', + onboardingStart: 'empezar', + onboardingCredit: 'Añadir créditos', + onboardingCreditDescription: 'DeepSeek Harness cobra créditos en función de los tokens utilizados por los modelos y herramientas. Agregue créditos por adelantado para mantener sus tareas ejecutándose sin interrupciones. Su saldo solo se utiliza mientras el agente está trabajando en una tarea.', + onboardingTopUp: 'Añadir créditos', + onboardingLater: 'Siguiente', + onboardingFundedTopUp: 'Añadir créditos', + onboardingPurposePrefix: '¿En qué quieres que ', + onboardingPurposeSuffix: ' te ayude?', + onboardingPurposeDescription: 'Adaptaremos la interfaz y las herramientas para que se adapten a su forma de trabajar.', + onboardingOffice: 'Oficina y trabajo creativo', + onboardingOfficeDescription: 'Edite documentos, organice datos, cree presentaciones y más', + onboardingDevelopment: 'Codificación y desarrollo', + onboardingDevelopmentDescription: 'Edite código, depure problemas, ejecute comandos, administre archivos de proyectos y más', + onboardingContinue: 'Continuar', + onboardingProcess: '¿Cuántos detalles te gustaría ver?', + onboardingProcessDescription: 'Esto solo cambia cómo se muestra el progreso, no lo que puede hacer DeepSeek Harness.', + onboardingCompact: 'Sólo resultados', + onboardingCompactDescription: 'Vea solo los resultados en una interfaz limpia y sencilla', + onboardingStandard: 'Detalles clave', + onboardingStandardDescription: 'Centrarse en los resultados, mostrando solo los pasos y acciones más importantes.', + onboardingDetailed: 'Detalles completos', + onboardingDetailedDescription: 'Vea el proceso completo para facilitar la depuración y la resolución de problemas', + onboardingEnter: 'Abrir aplicación', + onboardingBack: 'Atrás', + onboardingSkip: 'Saltar', + onboardingSkipTitle: '¿Saltar configuración?', + onboardingSkipDescription: 'Puede cambiar cómo se muestran el progreso, el rendimiento y el uso, o habilitar las herramientas de codificación, en cualquier momento en Configuración → General.', + onboardingKeepSetting: 'Continuar con la configuración', + onboardingNoCreditTitle: '¿Omitir agregar créditos?', + onboardingNoCreditDescription: 'DeepSeek Harness no puede iniciar nuevas tareas sin créditos. Puede agregarlos más tarde en Configuración → Cuenta.', + onboardingUnderstood: 'Lo tengo', + onboardingGoTopUp: 'Añadir créditos', + onboardingSaveFailed: 'No se pudo guardar la configuración. Intentar otra vez.', + onboardingRetry: 'Reintentar', + onboardingLoading: 'Cargando configuración…', +} satisfies typeof onboardingEn + +/** French dictionary, checked against the English keys. */ +export const onboardingFr = { + onboardingArtworkLocale: 'en', + onboardingWelcome: 'Bienvenue à', + onboardingBrand: 'DeepSeek Harness', + onboardingIntroduction: 'DeepSeek Harness fonctionne dans un dossier local et utilise des outils pour lire et écrire des fichiers sur votre ordinateur. Il peut vous aider à rechercher et à organiser des informations, à créer des documents et des feuilles de calcul, à écrire du code, à résoudre des problèmes, etc.', + onboardingStart: 'Commencez', + onboardingCredit: 'Ajouter des crédits', + onboardingCreditDescription: "DeepSeek Harness facture des crédits en fonction des jetons utilisés par les modèles et les outils. Ajoutez des crédits à l'avance pour que vos tâches continuent de s'exécuter sans interruption. Votre solde n'est utilisé que lorsque l'agent travaille sur une tâche.", + onboardingTopUp: 'Ajouter des crédits', + onboardingLater: 'Suivant', + onboardingFundedTopUp: 'Ajouter des crédits', + onboardingPurposePrefix: 'Que souhaitez-vous que ', + onboardingPurposeSuffix: ' fasse pour vous ?', + onboardingPurposeDescription: 'Nous adapterons l’interface et les outils à votre façon de travailler.', + onboardingOffice: 'Bureau et travail créatif', + onboardingOfficeDescription: 'Modifiez des documents, organisez des données, créez des présentations et bien plus encore', + onboardingDevelopment: 'Codage et développement', + onboardingDevelopmentDescription: 'Modifiez le code, déboguez les problèmes, exécutez des commandes, gérez les fichiers de projet, etc.', + onboardingContinue: 'Continuer', + onboardingProcess: 'Combien de détails aimeriez-vous voir ?', + onboardingProcessDescription: 'Cela change uniquement la façon dont la progression est affichée, pas ce que DeepSeek Harness peut faire.', + onboardingCompact: 'Résultats uniquement', + onboardingCompactDescription: 'Visualisez uniquement les résultats dans une interface claire et simple', + onboardingStandard: 'Détails clés', + onboardingStandardDescription: 'Concentrez-vous sur les résultats, en affichant uniquement les étapes et actions les plus importantes.', + onboardingDetailed: 'Tous les détails', + onboardingDetailedDescription: 'Consultez le processus complet pour faciliter le débogage et le dépannage', + onboardingEnter: "Ouvrir l'application", + onboardingBack: 'Retour', + onboardingSkip: 'Sauter', + onboardingSkipTitle: 'Ignorer la configuration ?', + onboardingSkipDescription: "Vous pouvez modifier la façon dont la progression, les performances et l'utilisation sont affichées, ou activer les outils de codage, à tout moment dans Paramètres → Général.", + onboardingKeepSetting: 'Continuer la configuration', + onboardingNoCreditTitle: "Ignorer l'ajout de crédits ?", + onboardingNoCreditDescription: 'DeepSeek Harness ne peut pas démarrer de nouvelles tâches sans crédits. Vous pourrez les ajouter plus tard dans Paramètres → Compte.', + onboardingUnderstood: "Je l'ai compris", + onboardingGoTopUp: 'Ajouter des crédits', + onboardingSaveFailed: "Impossible d'enregistrer les paramètres. Essayer à nouveau.", + onboardingRetry: 'Réessayer', + onboardingLoading: 'Chargement des paramètres…', +} satisfies typeof onboardingEn + +/** German dictionary, checked against the English keys. */ +export const onboardingDe = { + onboardingArtworkLocale: 'en', + onboardingWelcome: 'Willkommen bei', + onboardingBrand: 'DeepSeek Harness', + onboardingIntroduction: 'DeepSeek Harness arbeitet in einem lokalen Ordner und verwendet Tools zum Lesen und Schreiben von Dateien auf Ihrem Computer. Es kann Ihnen dabei helfen, Informationen zu recherchieren und zu organisieren, Dokumente und Tabellenkalkulationen zu erstellen, Code zu schreiben, Probleme zu beheben und vieles mehr.', + onboardingStart: 'Fangen Sie an', + onboardingCredit: 'Credits hinzufügen', + onboardingCreditDescription: 'DeepSeek Harness berechnet Credits basierend auf den von Modellen und Tools verwendeten Token. Fügen Sie Credits im Voraus hinzu, damit Ihre Aufgaben ohne Unterbrechung ausgeführt werden können. Ihr Guthaben wird nur verwendet, während der Agent an einer Aufgabe arbeitet.', + onboardingTopUp: 'Credits hinzufügen', + onboardingLater: 'Weiter', + onboardingFundedTopUp: 'Credits hinzufügen', + onboardingPurposePrefix: 'Wobei soll ', + onboardingPurposeSuffix: ' Ihnen helfen?', + onboardingPurposeDescription: 'Wir passen die Benutzeroberfläche und die Tools an Ihre Arbeitsweise an.', + onboardingOffice: 'Büro & kreative Arbeit', + onboardingOfficeDescription: 'Bearbeiten Sie Dokumente, organisieren Sie Daten, erstellen Sie Präsentationen und mehr', + onboardingDevelopment: 'Codierung und Entwicklung', + onboardingDevelopmentDescription: 'Bearbeiten Sie Code, debuggen Sie Probleme, führen Sie Befehle aus, verwalten Sie Projektdateien und mehr', + onboardingContinue: 'Weiter', + onboardingProcess: 'Wie viele Details möchten Sie sehen?', + onboardingProcessDescription: 'Dies ändert nur die Art und Weise, wie der Fortschritt angezeigt wird, nicht jedoch, was DeepSeek Harness tun kann.', + onboardingCompact: 'Nur Ergebnisse', + onboardingCompactDescription: 'Sehen Sie nur die Ergebnisse in einer übersichtlichen, einfachen Benutzeroberfläche', + onboardingStandard: 'Wichtige Details', + onboardingStandardDescription: 'Konzentrieren Sie sich auf die Ergebnisse und zeigen Sie nur die wichtigsten Schritte und Aktionen', + onboardingDetailed: 'Ausführliche Informationen', + onboardingDetailedDescription: 'Sehen Sie sich den vollständigen Prozess an, um das Debuggen und die Fehlerbehebung zu vereinfachen', + onboardingEnter: 'App öffnen', + onboardingBack: 'Zurück', + onboardingSkip: 'Überspringen', + onboardingSkipTitle: 'Einrichtung überspringen?', + onboardingSkipDescription: 'Sie können jederzeit unter Einstellungen → Allgemein ändern, wie Fortschritt, Leistung und Nutzung angezeigt werden, oder Codierungstools aktivieren.', + onboardingKeepSetting: 'Setzen Sie die Einrichtung fort', + onboardingNoCreditTitle: 'Hinzufügen von Credits überspringen?', + onboardingNoCreditDescription: 'DeepSeek Harness kann ohne Credits keine neuen Aufgaben starten. Sie können sie später unter Einstellungen → Konto hinzufügen.', + onboardingUnderstood: 'Verstanden', + onboardingGoTopUp: 'Credits hinzufügen', + onboardingSaveFailed: 'Die Einstellungen konnten nicht gespeichert werden. Versuchen Sie es erneut.', + onboardingRetry: 'Erneut versuchen', + onboardingLoading: 'Einstellungen werden geladen…', +} satisfies typeof onboardingEn + +/** Russian dictionary, checked against the English keys. */ +export const onboardingRu = { + onboardingArtworkLocale: 'en', + onboardingWelcome: 'Добро пожаловать в', + onboardingBrand: 'DeepSeek Harness', + onboardingIntroduction: 'DeepSeek Harness работает в локальной папке и использует инструменты для чтения и записи файлов на вашем компьютере. Он может помочь вам исследовать и систематизировать информацию, создавать документы и таблицы, писать код, устранять неполадки и многое другое.', + onboardingStart: 'Начать', + onboardingCredit: 'Добавить кредиты', + onboardingCreditDescription: 'DeepSeek Harness взимает кредиты на основе токенов, используемых моделями и инструментами. Добавляйте кредиты заранее, чтобы ваши задачи выполнялись без перебоев. Ваш баланс используется только пока агент работает над задачей.', + onboardingTopUp: 'Добавить кредиты', + onboardingLater: 'Далее', + onboardingFundedTopUp: 'Добавить кредиты', + onboardingPurposePrefix: 'С чем ', + onboardingPurposeSuffix: ' должен вам помочь?', + onboardingPurposeDescription: 'Мы адаптируем интерфейс и инструменты под ваш стиль работы.', + onboardingOffice: 'Офис и творческая работа', + onboardingOfficeDescription: 'Редактируйте документы, систематизируйте данные, создавайте презентации и многое другое.', + onboardingDevelopment: 'Кодирование и разработка', + onboardingDevelopmentDescription: 'Редактируйте код, устраняйте проблемы, запускайте команды, управляйте файлами проекта и многое другое.', + onboardingContinue: 'Продолжить', + onboardingProcess: 'Насколько подробно показывать процесс работы?', + onboardingProcessDescription: 'Это меняет только способ отображения прогресса, а не возможности DeepSeek Harness.', + onboardingCompact: 'Только результаты', + onboardingCompactDescription: 'Смотрите только результаты в понятном и простом интерфейсе', + onboardingStandard: 'Ключевые детали', + onboardingStandardDescription: 'Сосредоточьтесь на результатах, показывая только самые важные шаги и действия.', + onboardingDetailed: 'Полная информация', + onboardingDetailedDescription: 'Ознакомьтесь с полным процессом для упрощения отладки и устранения неполадок.', + onboardingEnter: 'Открыть приложение', + onboardingBack: 'Назад', + onboardingSkip: 'Пропустить', + onboardingSkipTitle: 'Пропустить настройку?', + onboardingSkipDescription: 'Вы можете изменить способ отображения прогресса, производительности и использования или включить инструменты программирования в любое время в «Настройки» → «Основные».', + onboardingKeepSetting: 'Продолжить настройку', + onboardingNoCreditTitle: 'Пропустить добавление кредитов?', + onboardingNoCreditDescription: 'DeepSeek Harness не может начинать новые задачи без кредитов. Вы можете добавить их позже в «Настройки» → «Аккаунт».', + onboardingUnderstood: 'понял', + onboardingGoTopUp: 'Добавить кредиты', + onboardingSaveFailed: 'Не удалось сохранить настройки. Попробуйте еще раз.', + onboardingRetry: 'Повторить', + onboardingLoading: 'Загрузка настроек…', +} satisfies typeof onboardingEn + +/** Portuguese dictionary, checked against the English keys. */ +export const onboardingPt = { + onboardingArtworkLocale: 'en', + onboardingWelcome: 'Bem-vindo ao', + onboardingBrand: 'DeepSeek Harness', + onboardingIntroduction: 'DeepSeek Harness funciona em uma pasta local e usa ferramentas para ler e gravar arquivos no seu computador. Ele pode ajudá-lo a pesquisar e organizar informações, criar documentos e planilhas, escrever códigos, solucionar problemas e muito mais.', + onboardingStart: 'Comece', + onboardingCredit: 'Adicionar créditos', + onboardingCreditDescription: 'DeepSeek Harness cobra créditos com base nos tokens usados pelos modelos e ferramentas. Adicione créditos antecipadamente para manter suas tarefas funcionando sem interrupção. Seu saldo só é usado enquanto o agente está trabalhando em uma tarefa.', + onboardingTopUp: 'Adicionar créditos', + onboardingLater: 'Próximo', + onboardingFundedTopUp: 'Adicionar créditos', + onboardingPurposePrefix: 'Em que você quer que ', + onboardingPurposeSuffix: ' ajude?', + onboardingPurposeDescription: 'Adaptaremos a interface e as ferramentas à sua maneira de trabalhar.', + onboardingOffice: 'Escritório e trabalho criativo', + onboardingOfficeDescription: 'Edite documentos, organize dados, crie apresentações e muito mais', + onboardingDevelopment: 'Codificação e desenvolvimento', + onboardingDevelopmentDescription: 'Edite código, depure problemas, execute comandos, gerencie arquivos de projeto e muito mais', + onboardingContinue: 'Continuar', + onboardingProcess: 'Quantos detalhes você gostaria de ver?', + onboardingProcessDescription: 'Isso altera apenas a forma como o progresso é mostrado, e não o que DeepSeek Harness pode fazer.', + onboardingCompact: 'Somente resultados', + onboardingCompactDescription: 'Veja apenas os resultados em uma interface limpa e simples', + onboardingStandard: 'Detalhes principais', + onboardingStandardDescription: 'Foco nos resultados, mostrando apenas as etapas e ações mais importantes', + onboardingDetailed: 'Detalhes completos', + onboardingDetailedDescription: 'Veja o processo completo para depuração e solução de problemas mais fáceis', + onboardingEnter: 'Abrir aplicativo', + onboardingBack: 'Voltar', + onboardingSkip: 'Pular', + onboardingSkipTitle: 'Ignorar configuração?', + onboardingSkipDescription: 'Você pode alterar a forma como o progresso, o desempenho e o uso são exibidos ou ativar as ferramentas de codificação a qualquer momento em Configurações → Geral.', + onboardingKeepSetting: 'Continuar configuração', + onboardingNoCreditTitle: 'Pular a adição de créditos?', + onboardingNoCreditDescription: 'DeepSeek Harness não pode iniciar novas tarefas sem créditos. Você pode adicioná-los posteriormente em Configurações → Conta.', + onboardingUnderstood: 'Entendi', + onboardingGoTopUp: 'Adicionar créditos', + onboardingSaveFailed: 'Não foi possível salvar as configurações. Tente novamente.', + onboardingRetry: 'Tentar novamente', + onboardingLoading: 'Carregando configurações…', +} satisfies typeof onboardingEn diff --git a/packages/client/ui-settings-agent-loop/locale/de.json b/packages/client/ui-settings-agent-loop/locale/de.json new file mode 100644 index 0000000000..10d2a14a6b --- /dev/null +++ b/packages/client/ui-settings-agent-loop/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Einstellungen für die Ausführung des Agenten", + "description": "Konfigurieren Sie die maximale Anzahl an Werkzeugaufrufen, die gleichzeitig ausgeführt werden können." + } +} diff --git a/packages/client/ui-settings-agent-loop/locale/es.json b/packages/client/ui-settings-agent-loop/locale/es.json new file mode 100644 index 0000000000..816970df3b --- /dev/null +++ b/packages/client/ui-settings-agent-loop/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configuración de ejecución del agente", + "description": "Configure el número máximo de llamadas a herramientas que se pueden ejecutar simultáneamente." + } +} diff --git a/packages/client/ui-settings-agent-loop/locale/fr.json b/packages/client/ui-settings-agent-loop/locale/fr.json new file mode 100644 index 0000000000..0ac781ec2b --- /dev/null +++ b/packages/client/ui-settings-agent-loop/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Paramètres d'exécution de l'agent", + "description": "Configurez le nombre maximum d'appels d'outils pouvant être exécutés simultanément." + } +} diff --git a/packages/client/ui-settings-agent-loop/locale/pt.json b/packages/client/ui-settings-agent-loop/locale/pt.json new file mode 100644 index 0000000000..72655d1257 --- /dev/null +++ b/packages/client/ui-settings-agent-loop/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurações de execução do agente", + "description": "Configure o número máximo de chamadas de ferramenta que podem ser executadas simultaneamente." + } +} diff --git a/packages/client/ui-settings-agent-loop/locale/ru.json b/packages/client/ui-settings-agent-loop/locale/ru.json new file mode 100644 index 0000000000..d07509407c --- /dev/null +++ b/packages/client/ui-settings-agent-loop/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Настройки запуска агента", + "description": "Настройте максимальное количество вызовов инструментов, которые могут выполняться одновременно." + } +} diff --git a/packages/client/ui-settings-agent-loop/src/client/index.ts b/packages/client/ui-settings-agent-loop/src/client/index.ts index 29ebe0e132..1e5e796bc3 100644 --- a/packages/client/ui-settings-agent-loop/src/client/index.ts +++ b/packages/client/ui-settings-agent-loop/src/client/index.ts @@ -16,7 +16,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type { Context as ClientContext } from '@deepseek-ai/cordis' import { AgentLoopCard } from './AgentLoopCard.tsx' import { AGENT_LOOP_NS, AgentLoopCardController } from './agent-loop-card-controller.ts' -import { en, ja, zh, type AgentLoopSettingsLocaleKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type AgentLoopSettingsLocaleKey } from './locales.ts' export type { AgentLoopCardProps } from './AgentLoopCard.tsx' export type { AgentLoopCardFace, AgentLoopCardState, AgentLoopSettings } from './agent-loop-card-controller.ts' @@ -40,8 +40,8 @@ export const inject = ['slots', 'locale', 'configForms'] * @param ctx - the browser plugin context. */ export function apply(ctx: ClientContext): void { + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-settings-agent-loop: dictionaries') const t = ctx.locale.bind(NS) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-settings-agent-loop: dictionaries') const card = new AgentLoopCardController(ctx.configForms.get(AGENT_LOOP_NS)) ctx.effect(() => () => { card.dispose() }, 'ui-settings-agent-loop: form subscription') ctx.effect(() => ctx.configForms.whileServed([AGENT_LOOP_NS], () => ctx.slots.inject('plugins.item', () => ctx.slots.register({ diff --git a/packages/client/ui-settings-agent-loop/src/client/locales.ts b/packages/client/ui-settings-agent-loop/src/client/locales.ts index e00a191c35..e9b79dd83f 100644 --- a/packages/client/ui-settings-agent-loop/src/client/locales.ts +++ b/packages/client/ui-settings-agent-loop/src/client/locales.ts @@ -64,3 +64,83 @@ export const ja = { 'saveFailed': 'デプロイがこれらの値を受け付けませんでした。修正のために残されています。', 'invalidNumber': '数値を入力するか、空欄のままにするとデフォルトが使用されます。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'Bucle de agente', + description: 'Controle cómo el Agente distribuye las llamadas a la herramienta.', + maxParallel: 'Llamadas a herramientas paralelas', + maxParallelHint: 'Límite superior de llamadas seguras en paralelo que se ejecutan al mismo tiempo en un solo paso.', + overridden: 'Anulado', + reset: 'Restablecer los valores predeterminados', + readOnly: 'Esta implementación almacena configuraciones de solo lectura.', + unavailable: 'Este complemento no está cargado, por lo que no se puede configurar en este momento.', + save: 'Guardar', + saving: 'Guardando…', + saveFailed: 'El despliegue no aceptó estos valores; quedaron para que usted los corrigiera.', + invalidNumber: 'Ingrese un número o déjelo en blanco para usar el valor predeterminado.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: "Boucle d'agent", + description: "Contrôlez la manière dont l'agent distribue les appels d'outil.", + maxParallel: "Appels d'outils parallèles", + maxParallelHint: 'Limite supérieure des appels parallèles sécurisés exécutés simultanément en une seule étape.', + overridden: 'Remplacé', + reset: 'Réinitialiser aux valeurs par défaut', + readOnly: 'Ce déploiement stocke les paramètres en lecture seule.', + unavailable: "Ce plugin n'est pas chargé, il ne peut donc pas être configuré pour le moment.", + save: 'Enregistrer', + saving: 'Sauvegarde…', + saveFailed: "Le déploiement n'a pas accepté ces valeurs ; ils vous ont été laissés à corriger.", + invalidNumber: 'Entrez un numéro ou laissez ce champ vide pour utiliser la valeur par défaut.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'Agentenschleife', + description: 'Steuern Sie, wie der Agent Toolaufrufe verteilt.', + maxParallel: 'Parallele Werkzeugaufrufe', + maxParallelHint: 'Obergrenze für parallelsichere Aufrufe, die gleichzeitig innerhalb eines Schritts ausgeführt werden.', + overridden: 'Überschrieben', + reset: 'Auf Standard zurücksetzen', + readOnly: 'Diese Bereitstellung speichert Einstellungen schreibgeschützt.', + unavailable: 'Dieses Plugin ist nicht geladen und kann daher derzeit nicht konfiguriert werden.', + save: 'Speichern', + saving: 'Speichern…', + saveFailed: 'Die Bereitstellung hat diese Werte nicht akzeptiert. Sie mussten sie korrigieren.', + invalidNumber: 'Geben Sie eine Zahl ein oder lassen Sie das Feld leer, um den Standardwert zu verwenden.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'Цикл агента', + description: 'Управляйте тем, как агент распределяет вызовы инструментов.', + maxParallel: 'Параллельные вызовы инструментов', + maxParallelHint: 'Верхняя граница параллельных безопасных вызовов, выполняемых одновременно в течение одного шага.', + overridden: 'Переопределено', + reset: 'Сбросить настройки по умолчанию', + readOnly: 'В этом развертывании параметры сохраняются только для чтения.', + unavailable: 'Этот плагин не загружен, поэтому его невозможно настроить прямо сейчас.', + save: 'Сохранить', + saving: 'Сохранение…', + saveFailed: 'Развертывание не приняло эти значения; их оставили вам на исправление.', + invalidNumber: 'Введите число или оставьте поле пустым, чтобы использовать значение по умолчанию.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'Loop de agente', + description: 'Controle como o Agente despacha chamadas de ferramenta.', + maxParallel: 'Chamadas de ferramentas paralelas', + maxParallelHint: 'Limite superior em chamadas paralelas seguras executadas ao mesmo tempo em uma etapa.', + overridden: 'Substituído', + reset: 'Redefinir para o padrão', + readOnly: 'Esta implantação armazena configurações somente leitura.', + unavailable: 'Este plugin não está carregado, portanto não pode ser configurado agora.', + save: 'Salvar', + saving: 'Salvando…', + saveFailed: 'A implantação não aceitou esses valores; eles foram deixados para você corrigir.', + invalidNumber: 'Insira um número ou deixe em branco para usar o padrão.', +} satisfies Record diff --git a/packages/client/ui-settings-general/locale/de.json b/packages/client/ui-settings-general/locale/de.json new file mode 100644 index 0000000000..e2d9ba29cd --- /dev/null +++ b/packages/client/ui-settings-general/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Allgemeine Einstellungen und Willkommenstipps", + "description": "Bietet allgemeine Einstellungen, Einstellungsfenster zum Verfassen von Texten und Tipps für die erstmalige Verwendung." + } +} diff --git a/packages/client/ui-settings-general/locale/es.json b/packages/client/ui-settings-general/locale/es.json new file mode 100644 index 0000000000..2910fb258d --- /dev/null +++ b/packages/client/ui-settings-general/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configuración general y consejos de bienvenida", + "description": "Proporciona configuraciones generales, configuración de redacción de ventanas y consejos para el primer uso." + } +} diff --git a/packages/client/ui-settings-general/locale/fr.json b/packages/client/ui-settings-general/locale/fr.json new file mode 100644 index 0000000000..7114398623 --- /dev/null +++ b/packages/client/ui-settings-general/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Paramètres généraux et conseils de bienvenue", + "description": "Fournit des paramètres généraux, la configuration de la rédaction de la fenêtre et des conseils pour la première utilisation." + } +} diff --git a/packages/client/ui-settings-general/locale/pt.json b/packages/client/ui-settings-general/locale/pt.json new file mode 100644 index 0000000000..2ab4067b6f --- /dev/null +++ b/packages/client/ui-settings-general/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurações gerais e dicas de boas-vindas", + "description": "Fornece configurações gerais, configuração de direitos autorais de janela e dicas de uso pela primeira vez." + } +} diff --git a/packages/client/ui-settings-general/locale/ru.json b/packages/client/ui-settings-general/locale/ru.json new file mode 100644 index 0000000000..8755a8fd82 --- /dev/null +++ b/packages/client/ui-settings-general/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Общие настройки и приветственные советы", + "description": "Содержит общие настройки, настройки окна копирайтинга и советы по первому использованию." + } +} diff --git a/packages/client/ui-settings-general/src/client/index.ts b/packages/client/ui-settings-general/src/client/index.ts index 180df12f10..0d85892813 100644 --- a/packages/client/ui-settings-general/src/client/index.ts +++ b/packages/client/ui-settings-general/src/client/index.ts @@ -44,7 +44,7 @@ import { SettingsDocumentAction } from './SettingsDocumentAction.tsx' import type { SettingsDocumentActionInjected } from './SettingsDocumentAction.tsx' import { SettingsDocumentStore } from './settings-document-store.ts' import { HostReleasePrompt, type HostReleasePromptInjected } from './HostReleasePrompt.tsx' -import { en, ja, zh, type SettingsKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type SettingsKey } from './locales.ts' export type { CloseLabelProps, HeaderContentProps, TriggerContentProps, @@ -99,7 +99,7 @@ export function apply(ctx: ClientContext): void { ctx.slots.inject('settings.general.item', () => ctx.slots.register({ name: 'settings.general.item', id: 'current-version', order: 100, locale: NS, }, CurrentVersionRow)) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-settings-general: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-settings-general: dictionaries') const connection = ctx.get('connection') as ConnectionHandle const carrier = (globalThis as typeof globalThis & { dshDesktop?: { protocolVersion: number; updates?: DesktopUpdateBridge } }).dshDesktop const desktopUpdate = new DesktopUpdateSource(carrier?.protocolVersion === 1 ? carrier.updates : undefined) diff --git a/packages/client/ui-settings-general/src/client/locales.ts b/packages/client/ui-settings-general/src/client/locales.ts index 8fddbcf24a..58afc6bba7 100644 --- a/packages/client/ui-settings-general/src/client/locales.ts +++ b/packages/client/ui-settings-general/src/client/locales.ts @@ -140,3 +140,233 @@ export const ja = { 'hostRelease.reload': '再読み込みして新しいバージョンを読み込む', 'hostRelease.later': '後で', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'trigger': 'Configuración', + 'shortcut.open': 'Abrir configuración', + 'desktop.update.available': 'Actualizar', + 'desktop.update.checking': 'Buscando actualizaciones...', + 'desktop.update.progress': '{percent}%', + 'desktop.update.verifying': 'Verificando archivos de actualización...', + 'desktop.update.installing': 'Preparándose para reiniciar…', + 'desktop.update.ready': 'Instalar y reiniciar', + 'desktop.update.retry': 'Reintentar actualización', + 'desktop.update.versionDetail': '{label}: {version}', + 'desktop.update.downloadDetail': 'Descargando la actualización: {percent}%\nVersión de destino: {version}', + 'desktop.update.checkFailed': 'No se pudo buscar actualizaciones. Inténtelo de nuevo más tarde.', + 'desktop.update.downloadFailed': 'No se pudo descargar la actualización. Por favor inténtalo de nuevo.', + 'desktop.update.installFailed': 'No se pudo instalar la actualización. Inténtelo de nuevo más tarde.', + 'desktop.update.checkNetworkFailed': 'No se pudo buscar actualizaciones. Comprueba tu conexión y vuelve a intentarlo.', + 'desktop.update.downloadNetworkFailed': 'No se pudo descargar la actualización. Comprueba tu conexión y vuelve a intentarlo.', + 'desktop.update.installNetworkFailed': 'No se pudo instalar la actualización. Comprueba tu conexión y vuelve a intentarlo.', + 'desktop.update.fullscreenTimeout': 'No se pudo salir de la pantalla completa. Salga de la pantalla completa y luego vuelva a intentar la actualización.', + 'desktop.update.stopFailed': 'No se pudieron detener las tareas de forma segura. La actualización no se ha instalado. Inténtelo de nuevo más tarde.', + 'desktop.update.tasksChanged': 'Han comenzado nuevas tareas. Confirme nuevamente para detener las tareas y actualizar.', + 'desktop.update.tasksUnavailable': 'El estado de la tarea no está disponible. Intente actualizar nuevamente cuando el espacio de trabajo esté listo.', + 'title': 'Configuración', + 'close': 'Cerrar', + 'openDocument': 'Abrir archivo de configuración', + 'openDocument.error': 'No se pudo abrir el archivo de configuración', + 'general.nav': 'General', + 'general.currentVersion': 'Versión actual: {version}', + 'developerTools.title': 'Mostrar vista de codificación', + 'developerTools.error': 'No se pudo guardar. Por favor inténtalo de nuevo.', + 'developerTools.description': 'Muestra la trayectoria, las diferencias de código y todos los ajustes preestablecidos del agente.', + 'connection.error': 'desconectado', + 'connection.connecting': 'Reconectando', + 'connection.connected': 'Conectado', + 'connection.reconnect': 'Desconectado, vuelve a conectarte ahora', + 'connection.restart': 'Reconectándonos, reconectándonos ahora', + 'hostRelease.title': 'La versión del servicio cambió', + 'hostRelease.description': 'La versión del servicio cambió. Esta página detuvo las actualizaciones en vivo; recargar para cargar la nueva versión.', + 'hostRelease.service': 'Servicio', + 'hostRelease.page': 'Página', + 'hostRelease.unknown': 'Desconocido', + 'hostRelease.reload': 'Recargar para cargar la nueva versión.', + 'hostRelease.later': 'Ahora no', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'trigger': 'Paramètres', + 'shortcut.open': 'Ouvrir les paramètres', + 'desktop.update.available': 'Mise à jour', + 'desktop.update.checking': 'Vérification des mises à jour…', + 'desktop.update.progress': '{percent}%', + 'desktop.update.verifying': 'Vérification des fichiers de mise à jour…', + 'desktop.update.installing': 'Préparation du redémarrage…', + 'desktop.update.ready': 'Installer et redémarrer', + 'desktop.update.retry': 'Réessayez la mise à jour', + 'desktop.update.versionDetail': '{label} : {version}', + 'desktop.update.downloadDetail': 'Téléchargement de la mise à jour : {percent}%\nVersion cible : {version}', + 'desktop.update.checkFailed': 'Impossible de vérifier les mises à jour. Veuillez réessayer plus tard.', + 'desktop.update.downloadFailed': 'Impossible de télécharger la mise à jour. Veuillez réessayer.', + 'desktop.update.installFailed': "Impossible d'installer la mise à jour. Veuillez réessayer plus tard.", + 'desktop.update.checkNetworkFailed': 'Impossible de vérifier les mises à jour. Vérifiez votre connexion et réessayez.', + 'desktop.update.downloadNetworkFailed': 'Impossible de télécharger la mise à jour. Vérifiez votre connexion et réessayez.', + 'desktop.update.installNetworkFailed': "Impossible d'installer la mise à jour. Vérifiez votre connexion et réessayez.", + 'desktop.update.fullscreenTimeout': 'Impossible de quitter le mode plein écran. Quittez le mode plein écran, puis réessayez la mise à jour.', + 'desktop.update.stopFailed': "Impossible d'arrêter les tâches en toute sécurité. La mise à jour n'a pas été installée. Veuillez réessayer plus tard.", + 'desktop.update.tasksChanged': 'De nouvelles tâches ont commencé. Confirmez à nouveau pour arrêter les tâches et mettre à jour.', + 'desktop.update.tasksUnavailable': "L'état de la tâche n'est pas disponible. Essayez à nouveau de mettre à jour lorsque l'espace de travail est prêt.", + 'title': 'Paramètres', + 'close': 'Fermer', + 'openDocument': 'Ouvrir le fichier de configuration', + 'openDocument.error': "Impossible d'ouvrir le fichier de configuration", + 'general.nav': 'Général', + 'general.currentVersion': 'Version actuelle : {version}', + 'developerTools.title': 'Afficher la vue de codage', + 'developerTools.error': "Impossible d'enregistrer. Veuillez réessayer.", + 'developerTools.description': "Affiche la trajectoire, les différences de code et tous les préréglages d'agent", + 'connection.error': 'Déconnecté', + 'connection.connecting': 'Reconnexion', + 'connection.connected': 'Connecté', + 'connection.reconnect': 'Déconnecté, reconnectez-vous maintenant', + 'connection.restart': 'Reconnexion, reconnectez-vous maintenant', + 'hostRelease.title': 'La version du service a été modifiée', + 'hostRelease.description': 'La version du service a changé. Cette page a suspendu les mises à jour en direct ; recharger pour charger la nouvelle version.', + 'hostRelease.service': 'Service', + 'hostRelease.page': 'Pages', + 'hostRelease.unknown': 'Inconnu', + 'hostRelease.reload': 'Recharger pour charger la nouvelle version', + 'hostRelease.later': 'Pas maintenant', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'trigger': 'Einstellungen', + 'shortcut.open': 'Einstellungen öffnen', + 'desktop.update.available': 'Aktualisieren', + 'desktop.update.checking': 'Suche nach Updates…', + 'desktop.update.progress': '{percent}%', + 'desktop.update.verifying': 'Aktualisierungsdateien werden überprüft…', + 'desktop.update.installing': 'Neustart wird vorbereitet…', + 'desktop.update.ready': 'Installieren und neu starten', + 'desktop.update.retry': 'Update erneut versuchen', + 'desktop.update.versionDetail': '{label}: {version}', + 'desktop.update.downloadDetail': 'Update wird heruntergeladen: {percent}%\nZielversion: {version}', + 'desktop.update.checkFailed': 'Es konnte nicht nach Updates gesucht werden. Bitte versuchen Sie es später noch einmal.', + 'desktop.update.downloadFailed': 'Das Update konnte nicht heruntergeladen werden. Bitte versuchen Sie es erneut.', + 'desktop.update.installFailed': 'Das Update konnte nicht installiert werden. Bitte versuchen Sie es später noch einmal.', + 'desktop.update.checkNetworkFailed': 'Es konnte nicht nach Updates gesucht werden. Überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.', + 'desktop.update.downloadNetworkFailed': 'Das Update konnte nicht heruntergeladen werden. Überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.', + 'desktop.update.installNetworkFailed': 'Das Update konnte nicht installiert werden. Überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.', + 'desktop.update.fullscreenTimeout': 'Der Vollbildmodus konnte nicht beendet werden. Beenden Sie den Vollbildmodus und versuchen Sie dann erneut, das Update durchzuführen.', + 'desktop.update.stopFailed': 'Die Aufgaben konnten nicht sicher gestoppt werden. Das Update wurde nicht installiert. Bitte versuchen Sie es später noch einmal.', + 'desktop.update.tasksChanged': 'Neue Aufgaben haben begonnen. Bestätigen Sie erneut, um die Aufgaben zu stoppen und zu aktualisieren.', + 'desktop.update.tasksUnavailable': 'Der Aufgabenstatus ist nicht verfügbar. Versuchen Sie erneut, die Aktualisierung durchzuführen, wenn der Arbeitsbereich bereit ist.', + 'title': 'Einstellungen', + 'close': 'Schließen', + 'openDocument': 'Konfigurationsdatei öffnen', + 'openDocument.error': 'Die Konfigurationsdatei konnte nicht geöffnet werden', + 'general.nav': 'Allgemein', + 'general.currentVersion': 'Aktuelle Version: {version}', + 'developerTools.title': 'Codieransicht anzeigen', + 'developerTools.error': 'Konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.', + 'developerTools.description': 'Zeigt Ausführungsverlauf, Codeunterschiede und alle Agentenvoreinstellungen an', + 'connection.error': 'Nicht verbunden', + 'connection.connecting': 'Wieder verbinden', + 'connection.connected': 'Verbunden', + 'connection.reconnect': 'Verbindung getrennt, jetzt erneut verbinden', + 'connection.restart': 'Verbindung wiederherstellen, jetzt erneut verbinden', + 'hostRelease.title': 'Dienstversion geändert', + 'hostRelease.description': 'Die Dienstversion hat sich geändert. Diese Seite hat Live-Updates angehalten. Neu laden, um die neue Version zu laden.', + 'hostRelease.service': 'Service', + 'hostRelease.page': 'Seite', + 'hostRelease.unknown': 'Unbekannt', + 'hostRelease.reload': 'Neu laden, um die neue Version zu laden', + 'hostRelease.later': 'Nicht jetzt', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'trigger': 'Настройки', + 'shortcut.open': 'Открыть настройки', + 'desktop.update.available': 'Обновить', + 'desktop.update.checking': 'Проверка обновлений…', + 'desktop.update.progress': '{percent}%', + 'desktop.update.verifying': 'Проверка файлов обновлений…', + 'desktop.update.installing': 'Подготовка к перезапуску…', + 'desktop.update.ready': 'Установить и перезапустить', + 'desktop.update.retry': 'Повторить обновление', + 'desktop.update.versionDetail': '{label}: {version}', + 'desktop.update.downloadDetail': 'Загрузка обновления: {percent}%\n Целевая версия: {version}', + 'desktop.update.checkFailed': 'Не удалось проверить наличие обновлений. Пожалуйста, повторите попытку позже.', + 'desktop.update.downloadFailed': 'Не удалось загрузить обновление. Пожалуйста, попробуйте еще раз.', + 'desktop.update.installFailed': 'Не удалось установить обновление. Пожалуйста, повторите попытку позже.', + 'desktop.update.checkNetworkFailed': 'Не удалось проверить наличие обновлений. Проверьте подключение и повторите попытку.', + 'desktop.update.downloadNetworkFailed': 'Не удалось загрузить обновление. Проверьте подключение и повторите попытку.', + 'desktop.update.installNetworkFailed': 'Не удалось установить обновление. Проверьте подключение и повторите попытку.', + 'desktop.update.fullscreenTimeout': 'Не удалось выйти из полноэкранного режима. Выйдите из полноэкранного режима и повторите попытку обновления.', + 'desktop.update.stopFailed': 'Не удалось безопасно остановить задачи. Обновление не установлено. Пожалуйста, повторите попытку позже.', + 'desktop.update.tasksChanged': 'Начались новые задачи. Подтвердите еще раз, чтобы остановить выполнение задач и выполнить обновление.', + 'desktop.update.tasksUnavailable': 'Статус задачи недоступен. Попробуйте обновить еще раз, когда рабочая область будет готова.', + 'title': 'Настройки', + 'close': 'Закрыть', + 'openDocument': 'Открыть файл конфигурации', + 'openDocument.error': 'Не удалось открыть файл конфигурации', + 'general.nav': 'Общие', + 'general.currentVersion': 'Текущая версия: {version}.', + 'developerTools.title': 'Показать представление кодирования', + 'developerTools.error': 'Не удалось сохранить. Пожалуйста, попробуйте еще раз.', + 'developerTools.description': 'Показывает траекторию, различия в коде и все настройки агента.', + 'connection.error': 'Отключено', + 'connection.connecting': 'Повторное подключение', + 'connection.connected': 'Подключено', + 'connection.reconnect': 'Соединение отключено, подключитесь снова сейчас', + 'connection.restart': 'Повторное подключение, повторное подключение сейчас', + 'hostRelease.title': 'Версия сервиса изменена', + 'hostRelease.description': 'Версия сервиса изменилась. На этой странице приостановлены текущие обновления; перезагрузите, чтобы загрузить новую версию.', + 'hostRelease.service': 'Сервис', + 'hostRelease.page': 'Страница', + 'hostRelease.unknown': 'Неизвестно', + 'hostRelease.reload': 'Перезагрузите, чтобы загрузить новую версию', + 'hostRelease.later': 'Не сейчас', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'trigger': 'Configurações', + 'shortcut.open': 'Abrir configurações', + 'desktop.update.available': 'Atualizar', + 'desktop.update.checking': 'Verificando atualizações…', + 'desktop.update.progress': '{percent}%', + 'desktop.update.verifying': 'Verificando arquivos de atualização…', + 'desktop.update.installing': 'Preparando para reiniciar…', + 'desktop.update.ready': 'Instalar e reiniciar', + 'desktop.update.retry': 'Tentar atualizar novamente', + 'desktop.update.versionDetail': '{label}: {version}', + 'desktop.update.downloadDetail': 'Fazendo download da atualização: {percent}%\nVersão de destino: {version}', + 'desktop.update.checkFailed': 'Não foi possível verificar se há atualizações. Por favor, tente novamente mais tarde.', + 'desktop.update.downloadFailed': 'Não foi possível baixar a atualização. Por favor, tente novamente.', + 'desktop.update.installFailed': 'Não foi possível instalar a atualização. Por favor, tente novamente mais tarde.', + 'desktop.update.checkNetworkFailed': 'Não foi possível verificar se há atualizações. Verifique sua conexão e tente novamente.', + 'desktop.update.downloadNetworkFailed': 'Não foi possível baixar a atualização. Verifique sua conexão e tente novamente.', + 'desktop.update.installNetworkFailed': 'Não foi possível instalar a atualização. Verifique sua conexão e tente novamente.', + 'desktop.update.fullscreenTimeout': 'Não foi possível sair da tela cheia. Saia da tela inteira e tente atualizar novamente.', + 'desktop.update.stopFailed': 'Não foi possível interromper as tarefas com segurança. A atualização não foi instalada. Por favor, tente novamente mais tarde.', + 'desktop.update.tasksChanged': 'Novas tarefas foram iniciadas. Confirme novamente para interromper as tarefas e atualizar.', + 'desktop.update.tasksUnavailable': 'O status da tarefa não está disponível. Tente atualizar novamente quando o espaço de trabalho estiver pronto.', + 'title': 'Configurações', + 'close': 'Fechar', + 'openDocument': 'Abra o arquivo de configuração', + 'openDocument.error': 'Não foi possível abrir o arquivo de configuração', + 'general.nav': 'Geral', + 'general.currentVersion': 'Versão atual: {version}', + 'developerTools.title': 'Mostrar visualização de codificação', + 'developerTools.error': 'Não foi possível salvar. Por favor, tente novamente.', + 'developerTools.description': 'Mostra trajetória, diferenças de código e todas as predefinições do agente', + 'connection.error': 'Desconectado', + 'connection.connecting': 'Reconectando', + 'connection.connected': 'Conectado', + 'connection.reconnect': 'Desconectado, reconecte agora', + 'connection.restart': 'Reconectando, reconecte agora', + 'hostRelease.title': 'Versão do serviço alterada', + 'hostRelease.description': 'A versão do serviço mudou. Esta página pausou as atualizações ao vivo; reload para carregar a nova versão.', + 'hostRelease.service': 'Serviço', + 'hostRelease.page': 'Página', + 'hostRelease.unknown': 'Desconhecido', + 'hostRelease.reload': 'Recarregue para carregar a nova versão', + 'hostRelease.later': 'Agora não', +} satisfies Record diff --git a/packages/client/ui-settings-models/locale/de.json b/packages/client/ui-settings-models/locale/de.json new file mode 100644 index 0000000000..e37303da3b --- /dev/null +++ b/packages/client/ui-settings-models/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Modelleinstellungsschnittstelle", + "description": "Verwalten Sie Modelldienste und ihre Zugriffskonfigurationen und stellen Sie relevante Anleitungen bereit." + } +} diff --git a/packages/client/ui-settings-models/locale/es.json b/packages/client/ui-settings-models/locale/es.json new file mode 100644 index 0000000000..46272e2878 --- /dev/null +++ b/packages/client/ui-settings-models/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de configuración del modelo", + "description": "Administre los servicios modelo y sus configuraciones de acceso, y proporcione orientación relevante." + } +} diff --git a/packages/client/ui-settings-models/locale/fr.json b/packages/client/ui-settings-models/locale/fr.json new file mode 100644 index 0000000000..66fe99de82 --- /dev/null +++ b/packages/client/ui-settings-models/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de configuration du modèle", + "description": "Gérez les services modèles et leurs configurations d'accès, et fournissez des conseils pertinents." + } +} diff --git a/packages/client/ui-settings-models/locale/pt.json b/packages/client/ui-settings-models/locale/pt.json new file mode 100644 index 0000000000..bcbc2971af --- /dev/null +++ b/packages/client/ui-settings-models/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de configuração de modelo", + "description": "Gerencie serviços de modelo e suas configurações de acesso e forneça orientações relevantes." + } +} diff --git a/packages/client/ui-settings-models/locale/ru.json b/packages/client/ui-settings-models/locale/ru.json new file mode 100644 index 0000000000..cb419aea1a --- /dev/null +++ b/packages/client/ui-settings-models/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс настройки модели", + "description": "Управляйте сервисами моделей и конфигурациями доступа к ним, а также предоставляйте соответствующие рекомендации." + } +} diff --git a/packages/client/ui-settings-models/src/client/index.ts b/packages/client/ui-settings-models/src/client/index.ts index 37ad6919fe..d0b61802a5 100644 --- a/packages/client/ui-settings-models/src/client/index.ts +++ b/packages/client/ui-settings-models/src/client/index.ts @@ -26,7 +26,7 @@ import { WelcomeNoticeStore } from './welcome-store.ts' import { ModelsSettingsStore } from './store.ts' import { createModelsOperations } from './operations.ts' import { createSettingsSchemaOperations } from './schema-operations.ts' -import { en, ja, zh, type ModelsKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type ModelsKey } from './locales.ts' import { WELCOME_NOTICE_SETTINGS_NAMESPACE } from '../onboarding-copy.ts' import { Config, ONBOARDING_CONFIG_GLOBAL } from '../onboarding-config.ts' @@ -80,7 +80,7 @@ export function apply(ctx: ClientContext): void { const payload = page[ONBOARDING_CONFIG_GLOBAL] const configured = Config(payload === undefined ? {} : payload) const credentialOnboarding = configured.credentialOnboarding && !('dshDesktop' in globalThis) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-settings-models: copy dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-settings-models: copy dictionaries') const schema = createSettingsSchemaOperations(ctx.settingsSchema) // Bound once here, where the Remote namespaces are declared in this plugin's diff --git a/packages/client/ui-settings-models/src/client/locales.ts b/packages/client/ui-settings-models/src/client/locales.ts index d1bf8aceb3..533b7aed02 100644 --- a/packages/client/ui-settings-models/src/client/locales.ts +++ b/packages/client/ui-settings-models/src/client/locales.ts @@ -425,3 +425,708 @@ export const ja = { 'onboardingSaving': '保存中…', 'keyRequired': '続けるにはAPIキーを入力してください。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + nav: 'Modelos', + deepSeekAccount: 'Cuenta DeepSeek', + title: 'Modelos', + intro: 'Ingrese sus claves API para usar modelos de los siguientes proveedores.', + edit: 'Editar', + editProvider: 'Editar {provider}', + remove: 'Eliminar', + removeProvider: 'Eliminar {provider}', + deleteTitle: '¿Eliminar {provider}?', + deleteDescription: 'Al eliminar {provider} se elimina su configuración. Cualquier credencial que utilice se administra en otro lugar y se conservará.', + deleteDescriptionWithCredential: 'Al eliminar {provider} se elimina su configuración y la clave API almacenada.', + deleteConfirm: 'Eliminar {provider}', + deleting: 'Eliminando {provider}…', + add: 'Agregar proveedor de modelo', + addMode: 'como agregar', + addCatalog: 'Proveedor de modelos externo', + addCustom: 'Modelo personalizado API', + addCatalogHint: 'Elija OpenAI, Anthropic, Kimi u otro proveedor del catálogo integrado e ingrese su clave API.', + addCustomHint: 'Conecte un repetidor, un servidor autohospedado o cualquier otro punto final compatible con OpenAI o Anthropic mediante su URL base, protocolo y modelos.', + addCatalogExhausted: 'Cada proveedor de catálogo ya está configurado.', + addCustomUnavailable: 'No hay ningún protocolo API disponible para declarar.', + provider: 'Proveedor', + close: 'Cerrar', + cancel: 'Cancelar', + apply: 'Aplicar', + applying: 'Aplicando…', + savedProvider: 'Guardado {provider}.', + credentialConfigured: 'Clave API configurada', + credentialMissing: 'Falta la clave API', + accountSignedIn: 'Iniciado sesión', + readOnly: 'El documento de configuración es de solo lectura en esta implementación.', + loadFailed: 'Error al cargar el directorio de proveedores', + conflict: 'Alguien más cambió esta configuración mientras esta tarjeta estaba abierta. Ciérrelo y vuelva a abrirlo para editar los valores actuales.', + retry: 'Reintentar', + keyInput: 'Clave de API', + keyReveal: 'Mostrar secreto', + keyHide: 'Ocultar secreto', + keyPlaceholder: 'Ingrese su clave API', + keyPlaceholderFallback: 'Déjelo en blanco para usar env como autenticación ({hint})', + keyPlaceholderSaved: 'Déjelo en blanco para usar las credenciales guardadas', + keyFallbackMissing: 'No se detectó la variable de entorno. Ingrese una clave API o configure {hint} en su entorno.', + keyFallbackDetected: 'Se detectó var env. ({hint})', + keyFallbackDetectedHidden: 'Se detectó var env.', + keyFallbackUnchecked: 'El estado de la credencial ambiental no ha sido confirmado.', + keyHelp: 'Las credenciales guardadas se utilizan primero. Las alternativas de entorno se muestran aquí, en orden: {env}. Las fuentes son el entorno de inicio, $cwd/.env, luego $DSH_HOME/.env.', + authHelp: 'Detalles de autenticación', + customized: 'Configuraciones personalizadas', + baseUrl: 'URL base', + baseUrlDefault: 'Sin punto final predeterminado', + baseUrlEnvironment: 'Configuración del punto final del proveedor: {env}', + baseUrlConfigured: 'Punto final configurado por el proveedor', + baseUrlPlaceholder: '{url}', + deepSeekBaseUrl: 'https://api.deepseek.com/anthropic (Uso predeterminado DEEPSEEK_BASE_URL)', + deepSeekEndpointHint: 'Utilice un punto final API compatible con Anthropic Messages.', + models: 'Modelos', + modelsInherited: 'Usando los valores predeterminados del adaptador', + modelsCustomized: 'Catálogo de modelos personalizado', + resetModels: 'Restaurar valores predeterminados', + model: 'modelo', + modelId: 'ID del modelo', + modelName: 'Nombre para mostrar', + modelNamePlaceholder: 'Utiliza el ID del modelo cuando está vacío', + contextWindow: 'ventana contextual', + contextWindowPlaceholder: 'Utiliza el proveedor predeterminado', + maxTokens: 'Fichas de salida máxima', + maxTokensPlaceholder: 'Utiliza el proveedor predeterminado', + modelAdvanced: 'Opciones de modelo', + modelInputTypes: 'Tipos de entrada', + modelInputText: 'Texto', + modelInputImage: 'Imagen', + modelReasoning: 'Niveles de razonamiento', + modelReasoningLevel: 'Nivel', + modelReasoningValue: 'Valor API', + modelReasoningHelp: 'Acerca de los niveles de razonamiento', + modelReasoningHelpText: 'El valor de la API se envía en el campo de esfuerzo de razonamiento de la solicitud, como reasoning_effort de OpenAI. De forma predeterminada se usa el nombre del nivel; si la documentación de la API del proveedor del modelo especifica otro valor, úselo. Desactivado deshabilita el razonamiento y no necesita un valor; cualquier valor ya guardado en la configuración se conserva y se envía.', + modelReasoningNeedsLevel: 'Agregue al menos un nivel además de Desactivado o desactive los niveles de razonamiento.', + modelReasoningValueRequired: 'Cada nivel además de Off necesita un valor API y ningún valor puede estar vacío.', + modelReasoningAdd: 'Agregar nivel', + modelReasoningRemove: 'Eliminar nivel', + modelReasoningOffValuePlaceholder: 'Desactiva el razonamiento', + addModel: 'Agregar modelo', + removeModel: 'Eliminar modelo', + reorderModel: 'Reordenar modelo', + reorderModelHelp: 'Arrastre para reordenar o use las teclas de flecha hacia arriba y hacia abajo.', + modelsEmpty: 'No se mostrará ningún modelo en el selector. Las identificaciones que no figuran en la lista aún se pueden enviar directamente.', + keyBlank: 'Ingrese la clave API, o deje el campo vacío para conservar la almacenada.', + keyBlankNew: 'Ingrese la clave API o deje el campo vacío si este proveedor se autentica de otra manera.', + keyIllegalCharacters: 'Esta clave API no tiene un formato válido. Por favor compruébalo.', + modelIdRequired: 'Se requiere identificación del modelo.', + modelIdDuplicate: 'El ID del modelo debe ser único.', + modelNameInvalid: 'El nombre para mostrar no puede estar vacío.', + modelContextInvalid: 'La ventana de contexto debe tener un recuento positivo, como 131072, 256K o 1M.', + modelMaxTokensInvalid: 'Los tokens de salida máxima deben tener un recuento positivo, como 8192, 64K o 1M.', + advancedHint: 'Otros campos se encuentran en cordis.patch.yml; edite esa sección directamente.', + modelCapacityInvalid: 'Una capacidad debe ser un número, opcionalmente con el sufijo K o M.', + modelDuplicate: 'Cada ID de modelo puede aparecer una vez.', + fetchModels: 'Buscar modelos disponibles', + fetching: 'Preguntando al proveedor...', + fetchNeedsBaseUrl: 'Ingrese primero la URL base y luego busque.', + fetchEmpty: 'El proveedor no enumeró ningún modelo. Agréguelos a mano.', + fetchTitle: 'Elige modelos', + fetchDescription: 'Estos son los modelos que tiene disponibles este proveedor. Los modelos comprobados componen la lista.', + fetchSearch: 'Buscar modelos', + fetchNoMatches: 'No hay modelos coincidentes.', + fetchSelectAll: 'Seleccionar todo', + fetchDeselectAll: 'Deseleccionar todo', + fetchAdopt: 'Lista de actualización', + fetchKeepOne: 'Mantenga al menos un modelo controlado.', + customTag: 'personalizado', + customRoute: 'ID del proveedor', + customRouteHint: 'Identificador en minúscula, que comienza con una letra, que nombra de forma exclusiva a este proveedor en las solicitudes y como su nombre de credencial.', + customRouteInvalid: 'Comience con una letra minúscula; luego letras minúsculas, dígitos y guiones.', + customRouteTaken: 'Un proveedor ya utiliza esta identificación.', + customDisplayName: 'Nombre para mostrar', + customApi: 'Protocolo API', + customApiUnset: 'No seleccionado', + protocolOpenAiCompletions: 'Finalizaciones del chat OpenAI', + protocolOpenAiResponses: 'Respuestas de OpenAI', + protocolAnthropicMessages: 'Mensajes antrópicos', + customNeedsBaseUrl: 'Un proveedor personalizado necesita una URL base.', + customBaseUrlInvalid: 'Ingrese una URL HTTP o HTTPS válida.', + customNeedsModels: 'Un proveedor personalizado necesita al menos un modelo.', + customBaseUrlPlaceholder: 'https://gateway.example/v1', + customAnthropicBaseUrlPlaceholder: 'https://gateway.example', + settingsPathUnresolvable: 'ruta de configuración irresoluble', + create: 'Crear proveedor', + creating: 'Creando…', + welcomeTitle: 'Aviso previo', + welcomeBody: 'DeepSeek Harness 0.2 aún está en versión preliminar y muchas áreas necesitan mejoras y refinamientos continuos. Agradecemos los comentarios y sugerencias de todos los desarrolladores y usuarios. La nueva aplicación de escritorio ahora está dirigida a una amplia gama de usuarios, mientras que las funciones avanzadas relacionadas con los desarrolladores se pueden habilitar en la configuración. Se espera que las características del producto y las API de complementos de DeepSeek Harness continúen con una rápida iteración y evolución, y se estabilicen gradualmente con el tiempo.\n\nEsperamos explorar los límites de la inteligencia junto con usuarios y desarrolladores de todo el mundo, basándose en una infraestructura de código abierto, reutilizable y componible. Invitamos a todos a dar vida a sus ideas con DeepSeek Harness y participar en la comunidad para enriquecer el ecosistema de complementos.', + welcomeContinue: 'Continuar', + welcomeError: 'El reconocimiento no se pudo guardar. Por favor inténtalo de nuevo.', + onboardingTitle: 'Agregue una clave API para comenzar', + onboardingDescription: 'Configure el proveedor oficial DeepSeek para comenzar a construir.', + onboardingLater: 'Configurar más tarde', + onboardingSave: 'Guardar y continuar', + onboardingSaving: 'Guardando…', + keyRequired: 'Ingrese una clave API para continuar.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + nav: 'Modèles', + deepSeekAccount: 'Compte DeepSeek', + title: 'Modèles', + intro: 'Entrez vos clés API pour utiliser les modèles des fournisseurs suivants.', + edit: 'Modifier', + editProvider: 'Modifier {provider}', + remove: 'Supprimer', + removeProvider: 'Supprimer {provider}', + deleteTitle: 'Supprimer {provider} ?', + deleteDescription: "La suppression de {provider} supprime sa configuration. Tout identifiant qu'il utilise est géré ailleurs et sera conservé.", + deleteDescriptionWithCredential: 'La suppression de {provider} supprime sa configuration et la clé API stockée.', + deleteConfirm: 'Supprimer {provider}', + deleting: 'Suppression de {provider}…', + add: 'Ajouter un fournisseur de modèles', + addMode: 'Comment ajouter', + addCatalog: 'Fournisseur de modèles tiers', + addCustom: 'Modèle personnalisé API', + addCatalogHint: 'Choisissez OpenAI, Anthropic, Kimi ou un autre fournisseur dans le catalogue intégré et entrez sa clé API.', + addCustomHint: 'Connectez un relais, un serveur auto-hébergé ou tout autre point de terminaison compatible OpenAI ou Anthropic par son URL de base, son protocole et ses modèles.', + addCatalogExhausted: 'Chaque fournisseur de catalogue est déjà configuré.', + addCustomUnavailable: "Aucun protocole API n'est disponible à déclarer.", + provider: 'Fournisseur', + close: 'Fermer', + cancel: 'Annuler', + apply: 'Appliquer', + applying: 'Application…', + savedProvider: '{provider} enregistré.', + credentialConfigured: 'Clé API configurée', + credentialMissing: 'Clé API manquante', + accountSignedIn: 'Connecté', + readOnly: 'Le document de paramètres est en lecture seule dans ce déploiement.', + loadFailed: 'Le chargement du répertoire du fournisseur a échoué', + conflict: "Quelqu'un d'autre a modifié ces paramètres alors que cette carte était ouverte. Fermez-le et rouvrez-le pour modifier les valeurs actuelles.", + retry: 'Réessayer', + keyInput: 'Clé API', + keyReveal: 'Afficher le secret', + keyHide: 'Cacher le secret', + keyPlaceholder: 'Entrez votre clé API', + keyPlaceholderFallback: 'Laissez vide pour utiliser env comme authentification ({hint})', + keyPlaceholderSaved: "Laissez vide pour utiliser les informations d'identification enregistrées", + keyFallbackMissing: "Var d'environnement non détectée. Entrez une clé API ou configurez {hint} dans votre environnement.", + keyFallbackDetected: "Var d'environnement détectée. ({hint})", + keyFallbackDetectedHidden: "Var d'environnement détectée.", + keyFallbackUnchecked: 'Le statut des qualifications en environnement n’a pas été confirmé.', + keyHelp: "Les informations d'identification enregistrées sont utilisées en premier. Solutions de secours d’environnement affichées ici, dans l’ordre : {env}. Les sources sont l'environnement de lancement, $cwd/.env, puis $DSH_HOME/.env.", + authHelp: "Détails d'authentification", + customized: 'Paramètres personnalisés', + baseUrl: 'URL de base', + baseUrlDefault: 'Aucun point de terminaison par défaut', + baseUrlEnvironment: 'Configuration du point de terminaison du fournisseur : {env}', + baseUrlConfigured: 'Point de terminaison configuré par le fournisseur', + baseUrlPlaceholder: '{url}', + deepSeekBaseUrl: 'https://api.deepseek.com/anthropic (par défaut, DEEPSEEK_BASE_URL est utilisé)', + deepSeekEndpointHint: 'Utilisez un point de terminaison API compatible avec Anthropic Messages.', + models: 'Modèles', + modelsInherited: "Utilisation des valeurs par défaut de l'adaptateur", + modelsCustomized: 'Catalogue de modèles personnalisés', + resetModels: 'Restaurer les valeurs par défaut', + model: 'Modèle', + modelId: 'ID du modèle', + modelName: "Nom d'affichage", + modelNamePlaceholder: "Utilise l'ID du modèle lorsqu'il est vide", + contextWindow: 'Fenêtre contextuelle', + contextWindowPlaceholder: 'Utilise la valeur par défaut du fournisseur', + maxTokens: 'Jetons de sortie maximum', + maxTokensPlaceholder: 'Utilise la valeur par défaut du fournisseur', + modelAdvanced: 'Options de modèle', + modelInputTypes: "Types d'entrée", + modelInputText: 'Texte', + modelInputImage: 'Images', + modelReasoning: 'Niveaux de raisonnement', + modelReasoningLevel: 'Niveau', + modelReasoningValue: 'Valeur API', + modelReasoningHelp: 'À propos des niveaux de raisonnement', + modelReasoningHelpText: 'La valeur API est envoyée dans le champ d’effort de raisonnement de la requête, comme reasoning_effort chez OpenAI. Par défaut, elle correspond au nom du niveau ; si la documentation API du fournisseur du modèle indique une autre valeur, utilisez-la. Désactivé coupe le raisonnement et ne nécessite aucune valeur ; toute valeur déjà enregistrée dans la configuration est conservée et envoyée.', + modelReasoningNeedsLevel: 'Ajoutez au moins un niveau en plus de Désactivé ou désactivez les niveaux de raisonnement.', + modelReasoningValueRequired: 'Chaque niveau autre que Off nécessite une valeur API et aucune valeur ne peut être vide.', + modelReasoningAdd: 'Ajouter un niveau', + modelReasoningRemove: 'Supprimer le niveau', + modelReasoningOffValuePlaceholder: 'Désactive le raisonnement', + addModel: 'Ajouter un modèle', + removeModel: 'Supprimer le modèle', + reorderModel: 'Réorganiser le modèle', + reorderModelHelp: 'Faites glisser pour réorganiser ou utilisez les touches fléchées haut et bas.', + modelsEmpty: 'Aucun modèle ne sera affiché dans le sélecteur. Les identifiants non répertoriés peuvent toujours être envoyés directement.', + keyBlank: 'Saisissez la clé API, ou laissez le champ vide pour conserver celle stockée.', + keyBlankNew: "Saisissez la clé API ou laissez le champ vide si ce fournisseur s'authentifie d'une autre manière.", + keyIllegalCharacters: "Cette clé API n'est pas dans un format valide. Veuillez le vérifier.", + modelIdRequired: 'L’ID du modèle est requis.', + modelIdDuplicate: "L'ID du modèle doit être unique.", + modelNameInvalid: 'Le nom d’affichage ne peut pas être vide.', + modelContextInvalid: 'La fenêtre contextuelle doit être un nombre positif, comme 131072, 256K ou 1M.', + modelMaxTokensInvalid: 'Le nombre maximum de jetons de sortie doit être un nombre positif, comme 8192, 64K ou 1M.', + advancedHint: "D'autres champs se trouvent dans cordis.patch.yml ; modifiez cette section directement.", + modelCapacityInvalid: 'Une capacité doit être un nombre, éventuellement suivi du suffixe K ou M.', + modelDuplicate: 'Chaque ID de modèle peut apparaître une fois.', + fetchModels: 'Récupérer les modèles disponibles', + fetching: 'Demander au fournisseur…', + fetchNeedsBaseUrl: "Entrez d'abord l'URL de base, puis récupérez-la.", + fetchEmpty: 'Le fournisseur n’a répertorié aucun modèle. Ajoutez-les à la main.', + fetchTitle: 'Choisir des modèles', + fetchDescription: 'Ce sont les modèles disponibles chez ce fournisseur. Les modèles cochés constituent la liste.', + fetchSearch: 'Rechercher des modèles', + fetchNoMatches: 'Aucun modèle correspondant.', + fetchSelectAll: 'Tout sélectionner', + fetchDeselectAll: 'Tout désélectionner', + fetchAdopt: 'Mettre à jour la liste', + fetchKeepOne: 'Gardez au moins un modèle coché.', + customTag: 'Personnalisé', + customRoute: 'Identifiant du fournisseur', + customRouteHint: "Identifiant minuscule, commençant par une lettre, qui nomme de manière unique ce fournisseur dans les demandes et comme nom d'identification.", + customRouteInvalid: 'Commencez par une lettre minuscule ; puis des lettres minuscules, des chiffres et des tirets.', + customRouteTaken: 'Un fournisseur utilise déjà cet identifiant.', + customDisplayName: "Nom d'affichage", + customApi: 'Protocole API', + customApiUnset: 'Non sélectionné', + protocolOpenAiCompletions: 'Achèvements du chat OpenAI', + protocolOpenAiResponses: 'Réponses OpenAI', + protocolAnthropicMessages: 'Messages anthropiques', + customNeedsBaseUrl: "Un fournisseur personnalisé a besoin d'une URL de base.", + customBaseUrlInvalid: 'Saisissez une URL HTTP ou HTTPS valide.', + customNeedsModels: "Un fournisseur personnalisé a besoin d'au moins un modèle.", + customBaseUrlPlaceholder: 'https://gateway.example/v1', + customAnthropicBaseUrlPlaceholder: 'https://gateway.example', + settingsPathUnresolvable: 'chemin des paramètres insolubles', + create: 'Créer un fournisseur', + creating: 'Création…', + welcomeTitle: "Avis d'aperçu", + welcomeBody: "DeepSeek Harness 0.2 est toujours en version préliminaire et de nombreux domaines nécessitent une amélioration et un perfectionnement continus. Nous apprécions les commentaires et suggestions de tous les développeurs et utilisateurs. La nouvelle application de bureau cible désormais un large éventail d'utilisateurs, tandis que les fonctionnalités avancées liées aux développeurs peuvent être activées dans les paramètres. Les fonctionnalités du produit et les API de plug-in de DeepSeek Harness devraient poursuivre leurs itérations et leur évolution rapides, et se stabiliseront progressivement au fil du temps. \n\nNous sommes impatients d'explorer les limites de l'intelligence avec les utilisateurs et les développeurs du monde entier, en nous appuyant sur une infrastructure open source, réutilisable et composable. Nous invitons chacun à donner vie à ses idées avec DeepSeek Harness et à participer à la communauté pour enrichir l'écosystème des plugins.", + welcomeContinue: 'Continuer', + welcomeError: "L'accusé de réception n'a pas pu être enregistré. Veuillez réessayer.", + onboardingTitle: 'Ajoutez une clé API pour commencer', + onboardingDescription: 'Configurez le fournisseur officiel DeepSeek pour commencer la construction.', + onboardingLater: 'Configurer plus tard', + onboardingSave: 'Enregistrer et continuer', + onboardingSaving: 'Sauvegarde…', + keyRequired: 'Entrez une clé API pour continuer.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + nav: 'Modelle', + deepSeekAccount: 'DeepSeek-Konto', + title: 'Modelle', + intro: 'Geben Sie Ihre API-Schlüssel ein, um Modelle der folgenden Anbieter zu verwenden.', + edit: 'Bearbeiten', + editProvider: 'Bearbeiten Sie {provider}', + remove: 'Löschen', + removeProvider: 'Löschen Sie {provider}', + deleteTitle: '{provider} löschen?', + deleteDescription: 'Durch das Löschen von {provider} wird seine Konfiguration entfernt. Alle verwendeten Anmeldeinformationen werden an anderer Stelle verwaltet und aufbewahrt.', + deleteDescriptionWithCredential: 'Durch das Löschen von {provider} werden seine Konfiguration und der gespeicherte API-Schlüssel entfernt.', + deleteConfirm: 'Löschen Sie {provider}', + deleting: '{provider} wird gelöscht…', + add: 'Modellanbieter hinzufügen', + addMode: 'So fügen Sie hinzu', + addCatalog: 'Drittanbieter von Modellen', + addCustom: 'Kundenspezifisches Modell API', + addCatalogHint: 'Wählen Sie OpenAI, Anthropic, Kimi oder einen anderen Anbieter aus dem integrierten Katalog aus und geben Sie den API-Schlüssel ein.', + addCustomHint: 'Verbinden Sie ein Relay, einen selbst gehosteten Server oder einen anderen OpenAI- oder Anthropic-kompatiblen Endpunkt über seine Basis-URL, sein Protokoll und seine Modelle.', + addCatalogExhausted: 'Jeder Kataloganbieter ist bereits konfiguriert.', + addCustomUnavailable: 'Es ist kein API-Protokoll zur Deklaration verfügbar.', + provider: 'Anbieter', + close: 'Schließen', + cancel: 'Abbrechen', + apply: 'Anwenden', + applying: 'Wird angewendet…', + savedProvider: 'Gespeichert {provider}.', + credentialConfigured: 'API-Schlüssel konfiguriert', + credentialMissing: 'API-Schlüssel fehlt', + accountSignedIn: 'Angemeldet', + readOnly: 'Das Einstellungsdokument ist in dieser Bereitstellung schreibgeschützt.', + loadFailed: 'Das Laden des Anbieterverzeichnisses ist fehlgeschlagen', + conflict: 'Jemand anderes hat diese Einstellungen geändert, während diese Karte geöffnet war. Schließen Sie es und öffnen Sie es erneut, um die aktuellen Werte zu bearbeiten.', + retry: 'Erneut versuchen', + keyInput: 'API-Schlüssel', + keyReveal: 'Geheimnis zeigen', + keyHide: 'Geheimnis verbergen', + keyPlaceholder: 'Geben Sie Ihren API-Schlüssel ein', + keyPlaceholderFallback: 'Lassen Sie das Feld leer, um env als Authentifizierung zu verwenden ({hint})', + keyPlaceholderSaved: 'Lassen Sie das Feld leer, um gespeicherte Anmeldeinformationen zu verwenden', + keyFallbackMissing: 'Umgebungsvariable nicht erkannt. Geben Sie einen API-Schlüssel ein oder konfigurieren Sie {hint} in Ihrer Umgebung.', + keyFallbackDetected: 'Umgebungsvariable erkannt. ({hint})', + keyFallbackDetectedHidden: 'Umgebungsvariable erkannt.', + keyFallbackUnchecked: 'Der Status der Umweltberechtigung wurde nicht bestätigt.', + keyHelp: 'Gespeicherte Anmeldeinformationen werden zuerst verwendet. Hier werden Umgebungs-Fallbacks in der Reihenfolge angezeigt: {env}. Quellen sind die Startumgebung, $cwd/.env und dann $DSH_HOME/.env.', + authHelp: 'Authentifizierungsdetails', + customized: 'Benutzerdefinierte Einstellungen', + baseUrl: 'Basis-URL', + baseUrlDefault: 'Kein Standardendpunkt', + baseUrlEnvironment: 'Konfiguration des Anbieterendpunkts: {env}', + baseUrlConfigured: 'Vom Anbieter konfigurierter Endpunkt', + baseUrlPlaceholder: '{url}', + deepSeekBaseUrl: 'https://api.deepseek.com/anthropic (Standardmäßig wird DEEPSEEK_BASE_URL verwendet)', + deepSeekEndpointHint: 'Verwenden Sie einen API-Endpunkt, der mit Anthropic Messages kompatibel ist.', + models: 'Modelle', + modelsInherited: 'Verwendung der Adapterstandards', + modelsCustomized: 'Maßgeschneiderter Modellkatalog', + resetModels: 'Standardeinstellungen wiederherstellen', + model: 'Modell', + modelId: 'Modell-ID', + modelName: 'Anzeigename', + modelNamePlaceholder: 'Verwendet die Modell-ID, wenn sie leer ist', + contextWindow: 'Kontextfenster', + contextWindowPlaceholder: 'Verwendet die Standardeinstellung des Anbieters', + maxTokens: 'Maximale Anzahl ausgegebener Token', + maxTokensPlaceholder: 'Verwendet die Standardeinstellung des Anbieters', + modelAdvanced: 'Modelloptionen', + modelInputTypes: 'Eingabetypen', + modelInputText: 'Text', + modelInputImage: 'Bild', + modelReasoning: 'Argumentationsebenen', + modelReasoningLevel: 'Ebene', + modelReasoningValue: 'API-Wert', + modelReasoningHelp: 'Über Argumentationsebenen', + modelReasoningHelpText: 'Der API-Wert wird im Feld für den Denkaufwand der Anfrage gesendet, etwa reasoning_effort bei OpenAI. Standardmäßig wird der Stufenname verwendet; gibt die API-Dokumentation des Modellanbieters einen anderen Wert vor, verwenden Sie diesen. Aus deaktiviert das Denken und benötigt keinen Wert; ein bereits konfigurierter Wert bleibt erhalten und wird gesendet.', + modelReasoningNeedsLevel: 'Fügen Sie neben „Aus“ mindestens eine Ebene hinzu oder deaktivieren Sie die Argumentationsebenen.', + modelReasoningValueRequired: 'Jede Ebene außer Off benötigt einen API-Wert und kein Wert darf leer sein.', + modelReasoningAdd: 'Ebene hinzufügen', + modelReasoningRemove: 'Ebene löschen', + modelReasoningOffValuePlaceholder: 'Schaltet das Denken aus', + addModel: 'Modell hinzufügen', + removeModel: 'Modell löschen', + reorderModel: 'Modell neu anordnen', + reorderModelHelp: 'Ziehen Sie, um die Reihenfolge neu anzuordnen, oder verwenden Sie die Aufwärts- und Abwärtspfeiltasten.', + modelsEmpty: 'Im Selektor werden keine Modelle angezeigt. Nicht gelistete Ausweise können weiterhin direkt versendet werden.', + keyBlank: 'Geben Sie den API-Schlüssel ein oder lassen Sie das Feld leer, um den gespeicherten Schlüssel beizubehalten.', + keyBlankNew: 'Geben Sie den API-Schlüssel ein oder lassen Sie das Feld leer, wenn dieser Anbieter auf andere Weise authentifiziert.', + keyIllegalCharacters: 'Dieser API-Schlüssel hat kein gültiges Format. Bitte überprüfen Sie es.', + modelIdRequired: 'Modell-ID ist erforderlich.', + modelIdDuplicate: 'Die Modell-ID muss eindeutig sein.', + modelNameInvalid: 'Der Anzeigename darf nicht leer sein.', + modelContextInvalid: 'Das Kontextfenster muss eine positive Zahl sein, z. B. 131072, 256K oder 1M.', + modelMaxTokensInvalid: 'Die maximale Anzahl an Ausgabetokens muss eine positive Zahl sein, z. B. 8192, 64K oder 1M.', + advancedHint: 'Andere Felder befinden sich in cordis.patch.yml; Bearbeiten Sie diesen Abschnitt direkt.', + modelCapacityInvalid: 'Eine Kapazität muss eine Zahl sein, optional mit dem Suffix K oder M.', + modelDuplicate: 'Jede Modell-ID darf einmal vorkommen.', + fetchModels: 'Verfügbare Modelle abrufen', + fetching: 'Den Anbieter fragen...', + fetchNeedsBaseUrl: 'Geben Sie zuerst die Basis-URL ein und rufen Sie sie dann ab.', + fetchEmpty: 'Der Anbieter listete keine Modelle auf. Fügen Sie sie von Hand hinzu.', + fetchTitle: 'Wählen Sie Modelle', + fetchDescription: 'Dies sind die Modelle, die dieser Anbieter zur Verfügung hat. Geprüfte Modelle bilden die Liste.', + fetchSearch: 'Modelle suchen', + fetchNoMatches: 'Keine passenden Modelle.', + fetchSelectAll: 'Alles auswählen', + fetchDeselectAll: 'Alle abwählen', + fetchAdopt: 'Liste aktualisieren', + fetchKeepOne: 'Lassen Sie mindestens ein Modell überprüft.', + customTag: 'Benutzerdefiniert', + customRoute: 'Anbieter-ID', + customRouteHint: 'Bezeichner in Kleinbuchstaben, beginnend mit einem Buchstaben, der diesen Anbieter in Anfragen und als Anmeldeinformationsname eindeutig benennt.', + customRouteInvalid: 'Beginnen Sie mit einem Kleinbuchstaben; dann Kleinbuchstaben, Ziffern und Bindestriche.', + customRouteTaken: 'Ein Anbieter verwendet diese ID bereits.', + customDisplayName: 'Anzeigename', + customApi: 'API-Protokoll', + customApiUnset: 'Nicht ausgewählt', + protocolOpenAiCompletions: 'OpenAI-Chat-Abschlüsse', + protocolOpenAiResponses: 'OpenAI-Antworten', + protocolAnthropicMessages: 'Anthropische Botschaften', + customNeedsBaseUrl: 'Ein benutzerdefinierter Anbieter benötigt eine Basis-URL.', + customBaseUrlInvalid: 'Geben Sie eine gültige HTTP- oder HTTPS-URL ein.', + customNeedsModels: 'Ein benutzerdefinierter Anbieter benötigt mindestens ein Modell.', + customBaseUrlPlaceholder: 'https://gateway.example/v1', + customAnthropicBaseUrlPlaceholder: 'https://gateway.example', + settingsPathUnresolvable: 'Unauflösbarer Einstellungspfad', + create: 'Anbieter erstellen', + creating: 'Erstellen…', + welcomeTitle: 'Vorschauhinweis', + welcomeBody: 'DeepSeek Harness 0.2 befindet sich noch in der Vorschau und viele Bereiche müssen weiter verbessert und verfeinert werden. Wir freuen uns über Feedback und Vorschläge aller Entwickler und Benutzer. Die neue Desktop-App richtet sich nun an ein breites Nutzerspektrum, während entwicklerbezogene erweiterte Funktionen in den Einstellungen aktiviert werden können. Es wird erwartet, dass die Produktfunktionen und Plugin-APIs von DeepSeek Harness ihre schnelle Iteration und Weiterentwicklung fortsetzen und sich im Laufe der Zeit allmählich stabilisieren. Wir heißen jeden willkommen, seine Ideen mit DeepSeek Harness zum Leben zu erwecken und sich an der Community zu beteiligen, um das Plugin-Ökosystem zu bereichern.', + welcomeContinue: 'Weiter', + welcomeError: 'Die Bestätigung konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.', + onboardingTitle: 'Fügen Sie einen API-Schlüssel hinzu, um loszulegen', + onboardingDescription: 'Konfigurieren Sie den offiziellen DeepSeek-Anbieter, um mit der Erstellung zu beginnen.', + onboardingLater: 'Später konfigurieren', + onboardingSave: 'Speichern und fortfahren', + onboardingSaving: 'Speichern…', + keyRequired: 'Geben Sie einen API-Schlüssel ein, um fortzufahren.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + nav: 'Модели', + deepSeekAccount: 'DeepSeek Аккаунт', + title: 'Модели', + intro: 'Введите ключи API, чтобы использовать модели следующих поставщиков.', + edit: 'Редактировать', + editProvider: 'Редактировать {provider}', + remove: 'Удалить', + removeProvider: 'Удалить {provider}', + deleteTitle: 'Удалить {provider}?', + deleteDescription: 'Удаление {provider} удаляет его конфигурацию. Любые учетные данные, которые он использует, управляются где-то еще и будут сохранены.', + deleteDescriptionWithCredential: 'Удаление {provider} удаляет его конфигурацию и сохраненный ключ API.', + deleteConfirm: 'Удалить {provider}', + deleting: 'Удаление {provider}…', + add: 'Добавить поставщика моделей', + addMode: 'Как добавить', + addCatalog: 'Сторонний поставщик моделей', + addCustom: 'Пользовательская модель API', + addCatalogHint: 'Выберите OpenAI, Anthropic, Kimi или другого провайдера из встроенного каталога и введите его ключ API.', + addCustomHint: 'Подключите реле, автономный сервер или любую другую конечную точку, совместимую с OpenAI или Anthropic, по ее базовому URL-адресу, протоколу и модели.', + addCatalogExhausted: 'Каждый поставщик каталога уже настроен.', + addCustomUnavailable: 'Протокол API недоступен для объявления.', + provider: 'Поставщик', + close: 'Закрыть', + cancel: 'Отмена', + apply: 'Применить', + applying: 'Применение…', + savedProvider: 'Сохранено {provider}.', + credentialConfigured: 'Ключ API настроен', + credentialMissing: 'Ключ API отсутствует', + accountSignedIn: 'Вошёл в систему', + readOnly: 'В этом развертывании документ настроек доступен только для чтения.', + loadFailed: 'Не удалось загрузить каталог поставщика', + conflict: 'Кто-то другой изменил эти настройки, пока эта карточка была открыта. Закройте его и откройте снова, чтобы редактировать текущие значения.', + retry: 'Повторить', + keyInput: 'Ключ API', + keyReveal: 'Показать секрет', + keyHide: 'Скрыть секрет', + keyPlaceholder: 'Введите свой ключ API', + keyPlaceholderFallback: 'Оставьте пустым, чтобы использовать env в качестве аутентификации ({hint})', + keyPlaceholderSaved: 'Оставьте поле пустым, чтобы использовать сохраненные учетные данные.', + keyFallbackMissing: 'Переменная Env не обнаружена. Введите ключ API или настройте {hint} в своей среде.', + keyFallbackDetected: 'Обнаружена переменная Env. ({hint})', + keyFallbackDetectedHidden: 'Обнаружена переменная Env.', + keyFallbackUnchecked: 'Статус экологической сертификации не подтвержден.', + keyHelp: 'Сохраненные учетные данные используются в первую очередь. Здесь показаны резервные варианты среды в порядке: {env}. Источниками являются среда запуска, $cwd/.env, затем $DSH_HOME/.env.', + authHelp: 'Детали аутентификации', + customized: 'Индивидуальные настройки', + baseUrl: 'Базовый URL', + baseUrlDefault: 'Нет конечной точки по умолчанию', + baseUrlEnvironment: 'Конфигурация конечной точки поставщика: {env}.', + baseUrlConfigured: 'Конечная точка, настроенная поставщиком', + baseUrlPlaceholder: '{url}', + deepSeekBaseUrl: 'https://api.deepseek.com/anthropic (по умолчанию используется DEEPSEEK_BASE_URL)', + deepSeekEndpointHint: 'Используйте конечную точку API, совместимую с антропными сообщениями.', + models: 'Модели', + modelsInherited: 'Использование настроек адаптера по умолчанию', + modelsCustomized: 'Индивидуальный каталог моделей', + resetModels: 'Восстановить настройки по умолчанию', + model: 'Модель', + modelId: 'Идентификатор модели', + modelName: 'Отображаемое имя', + modelNamePlaceholder: 'Использует идентификатор модели, когда он пуст', + contextWindow: 'Контекстное окно', + contextWindowPlaceholder: 'Использует поставщика по умолчанию', + maxTokens: 'Максимальное количество выходных токенов', + maxTokensPlaceholder: 'Использует поставщика по умолчанию', + modelAdvanced: 'Варианты модели', + modelInputTypes: 'Типы ввода', + modelInputText: 'Текст', + modelInputImage: 'Изображение', + modelReasoning: 'Уровни рассуждения', + modelReasoningLevel: 'Уровень', + modelReasoningValue: 'Значение API', + modelReasoningHelp: 'Об уровнях рассуждения', + modelReasoningHelpText: 'Значение API передаётся в поле уровня рассуждений запроса, например reasoning_effort у OpenAI. По умолчанию используется название уровня; если документация API поставщика модели указывает другое значение, используйте его. Выкл. отключает рассуждения и не требует значения; уже записанное в конфигурации значение сохраняется и отправляется.', + modelReasoningNeedsLevel: 'Добавьте хотя бы один уровень помимо «Выкл.» или отключите уровни рассуждения.', + modelReasoningValueRequired: 'Для каждого уровня, кроме Off, требуется значение API, и ни одно значение не может быть пустым.', + modelReasoningAdd: 'Добавить уровень', + modelReasoningRemove: 'Удалить уровень', + modelReasoningOffValuePlaceholder: 'Отключает рассуждения', + addModel: 'Добавить модель', + removeModel: 'Удалить модель', + reorderModel: 'Изменить порядок модели', + reorderModelHelp: 'Перетащите, чтобы изменить порядок, или используйте клавиши со стрелками вверх и вниз.', + modelsEmpty: 'В селекторе не будут показаны модели. Неуказанные идентификаторы по-прежнему можно отправлять напрямую.', + keyBlank: 'Введите ключ API или оставьте поле пустым, чтобы сохранить сохраненный.', + keyBlankNew: 'Введите ключ API или оставьте поле пустым, если этот провайдер выполняет аутентификацию другим способом.', + keyIllegalCharacters: 'Этот ключ API имеет недопустимый формат. Пожалуйста, проверьте это.', + modelIdRequired: 'Требуется идентификатор модели.', + modelIdDuplicate: 'Идентификатор модели должен быть уникальным.', + modelNameInvalid: 'Отображаемое имя не может быть пустым.', + modelContextInvalid: 'Контекстное окно должно иметь положительное значение, например 131072, 256K или 1M.', + modelMaxTokensInvalid: 'Максимальное количество выходных токенов должно иметь положительное значение, например 8192, 64K или 1M.', + advancedHint: 'Остальные поля находятся в файле cordis.patch.yml; отредактируйте этот раздел напрямую.', + modelCapacityInvalid: 'Емкость должна быть числом, возможно с суффиксом K или M.', + modelDuplicate: 'Каждый идентификатор модели может появиться один раз.', + fetchModels: 'Получить доступные модели', + fetching: 'Спросил у провайдера...', + fetchNeedsBaseUrl: 'Сначала введите базовый URL-адрес, а затем извлеките его.', + fetchEmpty: 'Поставщик не указал моделей. Добавьте их вручную.', + fetchTitle: 'Выберите модели', + fetchDescription: 'Это модели, доступные у этого поставщика. Проверенные модели составляют список.', + fetchSearch: 'Поиск моделей', + fetchNoMatches: 'Нет подходящих моделей.', + fetchSelectAll: 'Выбрать все', + fetchDeselectAll: 'Отменить выбор всех', + fetchAdopt: 'Обновить список', + fetchKeepOne: 'Проверьте хотя бы одну модель.', + customTag: 'Пользовательский', + customRoute: 'Идентификатор поставщика', + customRouteHint: 'Идентификатор в нижнем регистре, начинающийся с буквы, который уникально называет этого поставщика в запросах и в качестве его учетного имени.', + customRouteInvalid: 'Начните со строчной буквы; затем строчные буквы, цифры и тире.', + customRouteTaken: 'Поставщик уже использует этот идентификатор.', + customDisplayName: 'Отображаемое имя', + customApi: 'Протокол API', + customApiUnset: 'Не выбрано', + protocolOpenAiCompletions: 'Завершения чата OpenAI', + protocolOpenAiResponses: 'Ответы OpenAI', + protocolAnthropicMessages: 'Антропные сообщения', + customNeedsBaseUrl: 'Пользовательскому поставщику нужен базовый URL-адрес.', + customBaseUrlInvalid: 'Введите действительный URL-адрес HTTP или HTTPS.', + customNeedsModels: 'Пользовательскому поставщику нужна хотя бы одна модель.', + customBaseUrlPlaceholder: 'https://gateway.example/v1', + customAnthropicBaseUrlPlaceholder: 'https://gateway.example', + settingsPathUnresolvable: 'неразрешимый путь настроек', + create: 'Создать поставщика', + creating: 'Создание…', + welcomeTitle: 'Уведомление о предварительном просмотре', + welcomeBody: 'DeepSeek Harness 0.2 все еще находится в предварительной версии, и многие области нуждаются в дальнейшем улучшении и доработке. Мы приветствуем отзывы и предложения от всех разработчиков и пользователей. Новое настольное приложение теперь ориентировано на широкий круг пользователей, а расширенные функции, связанные с разработчиком, можно включить в настройках. Ожидается, что функции продукта DeepSeek Harness и API-интерфейсы плагинов будут продолжать быстро меняться и развиваться и со временем будут постепенно стабилизироваться. \n\nМы надеемся изучить пределы интеллекта вместе с пользователями и разработчиками по всему миру, опираясь на многоразовую и компонуемую инфраструктуру с открытым исходным кодом. Мы приглашаем всех воплотить свои идеи в жизнь с помощью DeepSeek Harness и принять участие в сообществе, чтобы обогатить экосистему плагинов.', + welcomeContinue: 'Продолжить', + welcomeError: 'Не удалось сохранить подтверждение. Пожалуйста, попробуйте еще раз.', + onboardingTitle: 'Добавьте ключ API, чтобы начать работу.', + onboardingDescription: 'Настройте официального поставщика DeepSeek, чтобы начать сборку.', + onboardingLater: 'Настроить позже', + onboardingSave: 'Сохранить и продолжить', + onboardingSaving: 'Сохранение…', + keyRequired: 'Введите ключ API, чтобы продолжить.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + nav: 'Modelos', + deepSeekAccount: 'DeepSeek Conta', + title: 'Modelos', + intro: 'Insira suas chaves API para usar modelos dos seguintes fornecedores.', + edit: 'Editar', + editProvider: 'Editar {provider}', + remove: 'Excluir', + removeProvider: 'Excluir {provider}', + deleteTitle: 'Excluir {provider}?', + deleteDescription: 'Excluir {provider} remove sua configuração. Qualquer credencial usada é gerenciada em outro lugar e será mantida.', + deleteDescriptionWithCredential: 'A exclusão de {provider} remove sua configuração e a chave API armazenada.', + deleteConfirm: 'Excluir {provider}', + deleting: 'Excluindo {provider}…', + add: 'Adicionar provedor de modelo', + addMode: 'Como adicionar', + addCatalog: 'Provedor de modelo terceirizado', + addCustom: 'Modelo personalizado API', + addCatalogHint: 'Escolha OpenAI, Anthropic, Kimi ou outro provedor no catálogo integrado e insira sua chave API.', + addCustomHint: 'Conecte um retransmissor, um servidor auto-hospedado ou qualquer outro endpoint compatível com OpenAI ou Anthropic por meio de URL, protocolo e modelos básicos.', + addCatalogExhausted: 'Cada provedor de catálogo já está configurado.', + addCustomUnavailable: 'Nenhum protocolo API está disponível para declaração.', + provider: 'Provedor', + close: 'Fechar', + cancel: 'Cancelar', + apply: 'Aplicar', + applying: 'Aplicando…', + savedProvider: 'Salvo {provider}.', + credentialConfigured: 'API chave configurada', + credentialMissing: 'API chave faltando', + accountSignedIn: 'Conectado', + readOnly: 'O documento de configurações é somente leitura nesta implantação.', + loadFailed: 'Falha ao carregar o diretório do provedor', + conflict: 'Outra pessoa alterou essas configurações enquanto este cartão estava aberto. Feche-o e abra-o novamente para editar os valores atuais.', + retry: 'Tentar novamente', + keyInput: 'API chave', + keyReveal: 'Mostrar segredo', + keyHide: 'Ocultar segredo', + keyPlaceholder: 'Insira sua chave API', + keyPlaceholderFallback: 'Deixe em branco para usar env como autenticação ({hint})', + keyPlaceholderSaved: 'Deixe em branco para usar credenciais salvas', + keyFallbackMissing: 'Env var não detectado. Insira uma chave API ou configure {hint} em seu ambiente.', + keyFallbackDetected: 'Variável de ambiente detectada. ({hint})', + keyFallbackDetectedHidden: 'Variável de ambiente detectada.', + keyFallbackUnchecked: 'O status da credencial ambiental não foi confirmado.', + keyHelp: 'As credenciais salvas são usadas primeiro. Fallbacks de ambiente mostrados aqui, em ordem: {env}. As fontes são o ambiente de inicialização, $cwd/.env e, em seguida, $DSH_HOME/.env.', + authHelp: 'Detalhes de autenticação', + customized: 'Configurações personalizadas', + baseUrl: 'URL base', + baseUrlDefault: 'Nenhum endpoint padrão', + baseUrlEnvironment: 'Configuração do endpoint do provedor: {env}', + baseUrlConfigured: 'Endpoint configurado pelo provedor', + baseUrlPlaceholder: '{url}', + deepSeekBaseUrl: 'https://api.deepseek.com/anthropic (padrão para usar DEEPSEEK_BASE_URL)', + deepSeekEndpointHint: 'Use um endpoint API compatível com Anthropic Messages.', + models: 'Modelos', + modelsInherited: 'Usando os padrões do adaptador', + modelsCustomized: 'Catálogo de modelos personalizados', + resetModels: 'Restaurar padrões', + model: 'Modelo', + modelId: 'ID do modelo', + modelName: 'Nome de exibição', + modelNamePlaceholder: 'Usa o ID do modelo quando vazio', + contextWindow: 'Janela de contexto', + contextWindowPlaceholder: 'Usa o padrão do provedor', + maxTokens: 'Máximo de tokens de saída', + maxTokensPlaceholder: 'Usa o padrão do provedor', + modelAdvanced: 'Opções de modelo', + modelInputTypes: 'Tipos de entrada', + modelInputText: 'Texto', + modelInputImage: 'Imagem', + modelReasoning: 'Níveis de raciocínio', + modelReasoningLevel: 'Nível', + modelReasoningValue: 'valor API', + modelReasoningHelp: 'Sobre níveis de raciocínio', + modelReasoningHelpText: 'O valor da API é enviado no campo de esforço de raciocínio da solicitação, como reasoning_effort da OpenAI. Por padrão, usa-se o nome do nível; se a documentação da API do provedor do modelo especificar outro valor, use-o. Desativado desliga o raciocínio e não exige valor; qualquer valor já salvo na configuração é mantido e enviado.', + modelReasoningNeedsLevel: 'Adicione pelo menos um nível além de Desativado ou desative os níveis de raciocínio.', + modelReasoningValueRequired: 'Cada nível além de Desligado precisa de um valor API e nenhum valor pode ficar vazio.', + modelReasoningAdd: 'Adicionar nível', + modelReasoningRemove: 'Excluir nível', + modelReasoningOffValuePlaceholder: 'Desativa o raciocínio', + addModel: 'Adicionar modelo', + removeModel: 'Excluir modelo', + reorderModel: 'Reordenar modelo', + reorderModelHelp: 'Arraste para reordenar ou use as teclas de seta para cima e para baixo.', + modelsEmpty: 'Nenhum modelo será mostrado no seletor. IDs não listados ainda podem ser enviados diretamente.', + keyBlank: 'Insira a chave API ou deixe o campo vazio para manter a chave armazenada.', + keyBlankNew: 'Insira a chave API ou deixe o campo vazio se este provedor autenticar de outra forma.', + keyIllegalCharacters: 'Esta chave API não está em um formato válido. Por favor, verifique.', + modelIdRequired: 'O ID do modelo é obrigatório.', + modelIdDuplicate: 'O ID do modelo deve ser exclusivo.', + modelNameInvalid: 'O nome de exibição não pode ficar vazio.', + modelContextInvalid: 'A janela de contexto deve ter uma contagem positiva, como 131072, 256K ou 1M.', + modelMaxTokensInvalid: 'Os tokens de saída máximos devem ter uma contagem positiva, como 8192, 64K ou 1M.', + advancedHint: 'Outros campos residem em cordis.patch.yml; edite essa seção diretamente.', + modelCapacityInvalid: 'Uma capacidade deve ser um número, opcionalmente com o sufixo K ou M.', + modelDuplicate: 'Cada ID de modelo pode aparecer uma vez.', + fetchModels: 'Buscar modelos disponíveis', + fetching: 'Perguntando ao provedor…', + fetchNeedsBaseUrl: 'Insira o URL base primeiro e depois busque.', + fetchEmpty: 'O fornecedor não listou nenhum modelo. Adicione-os manualmente.', + fetchTitle: 'Escolha modelos', + fetchDescription: 'Estes são os modelos que este fornecedor tem disponíveis. Os modelos verificados compõem a lista.', + fetchSearch: 'Pesquisar modelos', + fetchNoMatches: 'Não há modelos correspondentes.', + fetchSelectAll: 'Selecionar tudo', + fetchDeselectAll: 'Desmarcar tudo', + fetchAdopt: 'Lista de atualização', + fetchKeepOne: 'Mantenha pelo menos um modelo verificado.', + customTag: 'Personalizado', + customRoute: 'ID do provedor', + customRouteHint: 'Identificador minúsculo, começando com uma letra, que nomeia exclusivamente esse provedor nas solicitações e como seu nome de credencial.', + customRouteInvalid: 'Comece com uma letra minúscula; em seguida, letras minúsculas, dígitos e travessões.', + customRouteTaken: 'Um provedor já usa esse ID.', + customDisplayName: 'Nome de exibição', + customApi: 'API protocolo', + customApiUnset: 'Não selecionado', + protocolOpenAiCompletions: 'Conclusões do bate-papo OpenAI', + protocolOpenAiResponses: 'Respostas OpenAI', + protocolAnthropicMessages: 'Mensagens Antrópicas', + customNeedsBaseUrl: 'Um provedor personalizado precisa de um URL base.', + customBaseUrlInvalid: 'Insira um URL HTTP ou HTTPS válido.', + customNeedsModels: 'Um provedor personalizado precisa de pelo menos um modelo.', + customBaseUrlPlaceholder: 'https://gateway.example/v1', + customAnthropicBaseUrlPlaceholder: 'https://gateway.example', + settingsPathUnresolvable: 'caminho de configurações insolúveis', + create: 'Criar provedor', + creating: 'Criando…', + welcomeTitle: 'Aviso de visualização', + welcomeBody: 'DeepSeek Harness 0.2 ainda está em versão prévia e muitas áreas precisam de melhorias e refinamentos contínuos. Agradecemos comentários e sugestões de todos os desenvolvedores e usuários. O novo aplicativo de desktop agora é direcionado a uma ampla gama de usuários, enquanto recursos avançados relacionados ao desenvolvedor podem ser habilitados nas configurações. Espera-se que os recursos do produto e as APIs de plug-in do DeepSeek Harness continuem com rápida iteração e evolução e se estabilizem gradualmente ao longo do tempo.\n\nEsperamos explorar os limites da inteligência junto com usuários e desenvolvedores em todo o mundo, desenvolvendo uma infraestrutura de código aberto, reutilizável e combinável. Convidamos todos a dar vida às suas ideias com DeepSeek Harness e participar da comunidade para enriquecer o ecossistema de plug-ins.', + welcomeContinue: 'Continuar', + welcomeError: 'A confirmação não pôde ser salva. Por favor, tente novamente.', + onboardingTitle: 'Adicione uma chave API para começar', + onboardingDescription: 'Configure o provedor DeepSeek oficial para começar a compilar.', + onboardingLater: 'Configurar mais tarde', + onboardingSave: 'Salvar e continuar', + onboardingSaving: 'Salvando…', + keyRequired: 'Insira uma chave de API para continuar.', +} satisfies Record diff --git a/packages/client/ui-settings-models/tests/components.client.spec.tsx b/packages/client/ui-settings-models/tests/components.client.spec.tsx index da48676b6f..08ce75a428 100644 --- a/packages/client/ui-settings-models/tests/components.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/components.client.spec.tsx @@ -1,3 +1,4 @@ +import { es, fr, de, ru, pt } from '../src/client/locales.ts' // @vitest-environment jsdom /** Section, setup-card, and hand-written editor behavior over a scripted wire face. */ import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react' @@ -2423,3 +2424,16 @@ it('renders the localized account row as signed in without an edit action', asyn expect(within(item).getByText(en.accountSignedIn)).toBeTruthy() expect(within(item).queryByText(en.edit)).toBeNull() }) + +it.each([es, fr, de, ru, pt])('keeps capacity examples in translated validation messages parseable', (dictionary) => { + for (const [key, examples] of [ + ['modelContextInvalid', ['131072', '256K', '1M']], + ['modelMaxTokensInvalid', ['8192', '64K', '1M']], + ] as const) { + for (const example of examples) { + const token = dictionary[key].match(new RegExp(`(? ctx.locale.register(NS, { zh, en, ja }), 'ui-settings-plugin-inventory: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-settings-plugin-inventory: dictionaries') const t = ctx.locale.bind(NS) const list: PluginInventorySettingsTabInjected['list'] = async () => { diff --git a/packages/client/ui-settings-plugin-inventory/src/client/locales.ts b/packages/client/ui-settings-plugin-inventory/src/client/locales.ts index 391896bcc0..d0d7480472 100644 --- a/packages/client/ui-settings-plugin-inventory/src/client/locales.ts +++ b/packages/client/ui-settings-plugin-inventory/src/client/locales.ts @@ -125,3 +125,208 @@ export const ja = { 'failed': '起動に失敗', 'unloading': 'アンロード中', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + tab: 'Lista de complementos', + loading: 'Leyendo complementos…', + clientSyncing: 'Sincronizando complementos en esta página…', + clientSyncFailed: 'Algunos complementos no se pudieron sincronizar en esta página. La habilitación del host no ha cambiado.', + clientSyncRetry: 'Reintentar esta página', + error: 'Los complementos no están disponibles temporalmente.', + metadataError: 'Error de metadatos del paquete: {error}', + retry: 'Reintentar', + search: 'Complementos de búsqueda', + empty: 'No hay complementos disponibles.', + emptySearch: 'No hay complementos coincidentes.', + presetTitle: 'Complementos de sesión', + presetSubtitle: 'Compuesto por sesión por ajustes preestablecidos de agente', + switcherLabel: 'Elija el agente preestablecido para inspeccionar', + presetOptionDefault: '{name} (predeterminado)', + presetOptionBroken: '{name} (no se pudo cargar)', + globalTitle: 'Complementos globales', + globalSubtitle: 'Compartido por el sistema y cada sesión.', + presetProvidedDetail: 'Discapacitados a nivel mundial; Los ajustes preestablecidos del agente lo proporcionan por sesión.', + enabledIn: 'Habilitado en', + matchesInOtherPresets: '{count} más coincidencias en otros ajustes preestablecidos: ', + enabledTag: 'Habilitado', + disabledTag: 'Discapacitado', + conditionalTag: 'Condicional', + presetEnabledTag: 'A través de ajustes preestablecidos', + failedTag: 'Fallido', + moduleLabel: 'Módulo', + fromPreset: 'De', + condition: 'Deshabilitado cuando', + configuration: 'Configuración', + runtime: 'Estado', + unobserved: 'no corriendo', + pending: 'Esperando dependencias', + loadingPhase: 'Cargando', + active: 'corriendo', + failed: 'No se pudo iniciar', + unloading: 'Descarga', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + tab: 'Liste des plugins', + loading: 'Plugins de lecture…', + clientSyncing: 'Synchronisation des plugins sur cette page…', + clientSyncFailed: "Certains plugins n'ont pas pu se synchroniser sur cette page. L'activation de l'hôte est inchangée.", + clientSyncRetry: 'Réessayez cette page', + error: 'Les plugins sont temporairement indisponibles.', + metadataError: 'Erreur de métadonnées du package : {error}', + retry: 'Réessayer', + search: 'Plugins de recherche', + empty: "Aucun plugin n'est disponible.", + emptySearch: 'Aucun plugin correspondant.', + presetTitle: 'Plugins de sessions', + presetSubtitle: "Composé par session par préréglages d'agent", + switcherLabel: "Choisissez le préréglage d'agent à inspecter", + presetOptionDefault: '{name} (par défaut)', + presetOptionBroken: '{name} (échec du chargement)', + globalTitle: 'Plugins globaux', + globalSubtitle: 'Partagé par le système et à chaque session', + presetProvidedDetail: "Handicapé à l'échelle mondiale ; les préréglages d'agent le fournissent par session", + enabledIn: 'Activé dans', + matchesInOtherPresets: "{count} plus de correspondances dans d'autres préréglages : ", + enabledTag: 'Activé', + disabledTag: 'Désactivé', + conditionalTag: 'Conditionnel', + presetEnabledTag: 'Via les préréglages', + failedTag: 'Échec', + moduleLabel: 'Module', + fromPreset: 'De', + condition: 'Désactivé quand', + configuration: 'Configuration', + runtime: 'Statut', + unobserved: 'Ne fonctionne pas', + pending: 'En attente de dépendances', + loadingPhase: 'Chargement', + active: 'Courir', + failed: 'Échec du démarrage', + unloading: 'Déchargement', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + tab: 'Plugin-Liste', + loading: 'Plugins lesen…', + clientSyncing: 'Plugins auf dieser Seite werden synchronisiert…', + clientSyncFailed: 'Einige Plugins konnten auf dieser Seite nicht synchronisiert werden. Die Host-Aktivierung bleibt unverändert.', + clientSyncRetry: 'Versuchen Sie es erneut mit dieser Seite', + error: 'Plugins sind vorübergehend nicht verfügbar.', + metadataError: 'Paketmetadatenfehler: {error}', + retry: 'Erneut versuchen', + search: 'Such-Plugins', + empty: 'Es sind keine Plugins verfügbar.', + emptySearch: 'Keine passenden Plugins.', + presetTitle: 'Sitzungs-Plugins', + presetSubtitle: 'Zusammengestellt pro Sitzung nach Agentenvoreinstellungen', + switcherLabel: 'Wählen Sie die zu überprüfende Agentenvoreinstellung aus', + presetOptionDefault: '{name} (Standard)', + presetOptionBroken: '{name} (Laden fehlgeschlagen)', + globalTitle: 'Globale Plugins', + globalSubtitle: 'Wird vom System und jeder Sitzung geteilt', + presetProvidedDetail: 'Weltweit deaktiviert; Agentenvoreinstellungen stellen es pro Sitzung bereit', + enabledIn: 'Aktiviert in', + matchesInOtherPresets: '{count} weitere Übereinstimmungen in anderen Voreinstellungen: ', + enabledTag: 'Aktiviert', + disabledTag: 'Deaktiviert', + conditionalTag: 'Bedingt', + presetEnabledTag: 'Über Voreinstellungen', + failedTag: 'Fehlgeschlagen', + moduleLabel: 'Modul', + fromPreset: 'Von', + condition: 'Deaktiviert, wenn', + configuration: 'Konfiguration', + runtime: 'Status', + unobserved: 'Läuft nicht', + pending: 'Warten auf Abhängigkeiten', + loadingPhase: 'Laden', + active: 'Laufen', + failed: 'Start fehlgeschlagen', + unloading: 'Entladen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + tab: 'Список плагинов', + loading: 'Чтение плагинов…', + clientSyncing: 'Синхронизация плагинов на этой странице…', + clientSyncFailed: 'Некоторые плагины не смогли синхронизироваться на этой странице. Включение хоста не изменилось.', + clientSyncRetry: 'Повторить эту страницу', + error: 'Плагины временно недоступны.', + metadataError: 'Ошибка метаданных пакета: {error}.', + retry: 'Повторить', + search: 'Поиск плагинов', + empty: 'Нет доступных плагинов.', + emptySearch: 'Нет подходящих плагинов.', + presetTitle: 'Плагины сеансов', + presetSubtitle: 'Составляется за сеанс с помощью предустановок агента', + switcherLabel: 'Выберите предустановку агента для проверки', + presetOptionDefault: '{name} (по умолчанию)', + presetOptionBroken: '{name} (не удалось загрузить)', + globalTitle: 'Глобальные плагины', + globalSubtitle: 'Совместно используется системой и каждым сеансом', + presetProvidedDetail: 'Отключено глобально; предустановки агента предусматривают это за сеанс', + enabledIn: 'Включено в', + matchesInOtherPresets: '{count} больше совпадений в других пресетах: ', + enabledTag: 'Включено', + disabledTag: 'Отключено', + conditionalTag: 'Условный', + presetEnabledTag: 'Через пресеты', + failedTag: 'Не удалось', + moduleLabel: 'Модуль', + fromPreset: 'От', + condition: 'Отключено, когда', + configuration: 'Конфигурация', + runtime: 'Статус', + unobserved: 'Не работает', + pending: 'Ожидание зависимостей', + loadingPhase: 'Загрузка', + active: 'Бег', + failed: 'Не удалось запустить', + unloading: 'Разгрузка', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + tab: 'Lista de plug-ins', + loading: 'Lendo plug-ins…', + clientSyncing: 'Sincronizando plug-ins nesta página…', + clientSyncFailed: 'Alguns plug-ins não puderam ser sincronizados nesta página. A ativação do host permanece inalterada.', + clientSyncRetry: 'Tente esta página novamente', + error: 'Os plug-ins estão temporariamente indisponíveis.', + metadataError: 'Erro de metadados do pacote: {error}', + retry: 'Tentar novamente', + search: 'Plug-ins de pesquisa', + empty: 'Nenhum plug-in está disponível.', + emptySearch: 'Nenhum plug-in correspondente.', + presetTitle: 'Plug-ins de sessão', + presetSubtitle: 'Composto por sessão por predefinições de agente', + switcherLabel: 'Escolha a predefinição do agente para inspecionar', + presetOptionDefault: '{name} (padrão)', + presetOptionBroken: '{name} (falha ao carregar)', + globalTitle: 'Plug-ins globais', + globalSubtitle: 'Compartilhado pelo sistema e por cada sessão', + presetProvidedDetail: 'Desativado globalmente; predefinições de agente fornecem isso por sessão', + enabledIn: 'Habilitado em', + matchesInOtherPresets: '{count} mais correspondências em outras predefinições: ', + enabledTag: 'Habilitado', + disabledTag: 'Desativado', + conditionalTag: 'Condicional', + presetEnabledTag: 'Por meio de predefinições', + failedTag: 'Falha', + moduleLabel: 'Módulo', + fromPreset: 'De', + condition: 'Desativado quando', + configuration: 'Configuração', + runtime: 'Estado', + unobserved: 'Não está em execução', + pending: 'Esperando por dependências', + loadingPhase: 'Carregando', + active: 'Correndo', + failed: 'Falha ao iniciar', + unloading: 'Descarregando', +} satisfies Record diff --git a/packages/client/ui-settings-plugins/locale/de.json b/packages/client/ui-settings-plugins/locale/de.json new file mode 100644 index 0000000000..9b3e3dbeb2 --- /dev/null +++ b/packages/client/ui-settings-plugins/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Integrierter Plug-in-Einstellungseingang", + "description": "Bietet den integrierten Plug-in-Eingang und seine Funktionsregisterkarte in den Einstellungen." + } +} diff --git a/packages/client/ui-settings-plugins/locale/es.json b/packages/client/ui-settings-plugins/locale/es.json new file mode 100644 index 0000000000..f06cce9794 --- /dev/null +++ b/packages/client/ui-settings-plugins/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Entrada de configuración de complementos incorporados", + "description": "Proporciona la entrada del complemento integrado y su pestaña de función en la configuración." + } +} diff --git a/packages/client/ui-settings-plugins/locale/fr.json b/packages/client/ui-settings-plugins/locale/fr.json new file mode 100644 index 0000000000..762ba220ef --- /dev/null +++ b/packages/client/ui-settings-plugins/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Entrée des paramètres du plug-in intégré", + "description": "Fournit l'entrée du plug-in intégré et sa page à onglet de fonction dans les paramètres." + } +} diff --git a/packages/client/ui-settings-plugins/locale/pt.json b/packages/client/ui-settings-plugins/locale/pt.json new file mode 100644 index 0000000000..4af280898e --- /dev/null +++ b/packages/client/ui-settings-plugins/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Entrada de configurações de plug-in integradas", + "description": "Fornece a entrada do plug-in integrada e sua guia de função nas configurações." + } +} diff --git a/packages/client/ui-settings-plugins/locale/ru.json b/packages/client/ui-settings-plugins/locale/ru.json new file mode 100644 index 0000000000..4fdd139aa0 --- /dev/null +++ b/packages/client/ui-settings-plugins/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Вход в настройки встроенного плагина", + "description": "Обеспечивает вход встроенного плагина и вкладку его функций в настройках." + } +} diff --git a/packages/client/ui-settings-plugins/src/client/index.ts b/packages/client/ui-settings-plugins/src/client/index.ts index 4897b7c503..b9db73a850 100644 --- a/packages/client/ui-settings-plugins/src/client/index.ts +++ b/packages/client/ui-settings-plugins/src/client/index.ts @@ -18,7 +18,7 @@ import type { Context as ClientContext } from '@deepseek-ai/cordis' import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' import { PluginsSettingsSection } from './PluginsSettingsSection.tsx' import type { PluginsSettingsSectionInjected, PluginsSettingsTabEntry } from './PluginsSettingsSection.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' export type { PluginsSettingsSectionInjected, PluginsSettingsSectionProps } from './PluginsSettingsSection.tsx' @@ -34,7 +34,7 @@ export const inject = ['slots', 'locale'] */ export function apply(ctx: ClientContext): void { const t = ctx.locale.bind(NS) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-settings-plugins: section dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-settings-plugins: section dictionaries') let tabsVersion = -1 let tabsRevision = -1 diff --git a/packages/client/ui-settings-plugins/src/client/locales.ts b/packages/client/ui-settings-plugins/src/client/locales.ts index 9d32663ee5..723b596309 100644 --- a/packages/client/ui-settings-plugins/src/client/locales.ts +++ b/packages/client/ui-settings-plugins/src/client/locales.ts @@ -29,3 +29,48 @@ export const ja = { 'tabs': 'プラグインビュー', 'empty': 'このデプロイメントにはプラグインビューがありません。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + nav: 'Complementos integrados', + title: 'Complementos integrados', + intro: 'Inspeccione los complementos que incluye esta implementación.', + tabs: 'Vistas de complementos', + empty: 'Esta implementación no expone vistas de complementos.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + nav: 'Plugins intégrés', + title: 'Plugins intégrés', + intro: 'Inspectez les plugins fournis par ce déploiement.', + tabs: 'Vues des plugins', + empty: "Ce déploiement n'expose aucune vue de plugin.", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + nav: 'Integrierte Plugins', + title: 'Integrierte Plugins', + intro: 'Überprüfen Sie die Plugins, die mit dieser Bereitstellung geliefert werden.', + tabs: 'Plugin-Ansichten', + empty: 'Diese Bereitstellung stellt keine Plugin-Ansichten bereit.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + nav: 'Встроенные плагины', + title: 'Встроенные плагины', + intro: 'Проверьте плагины, поставляемые в этом развертывании.', + tabs: 'Просмотры плагинов', + empty: 'Это развертывание не предоставляет никаких представлений плагинов.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + nav: 'Plug-ins integrados', + title: 'Plug-ins integrados', + intro: 'Inspecione os plug-ins fornecidos por esta implantação.', + tabs: 'Visualizações de plug-in', + empty: 'Esta implantação não expõe visualizações de plug-ins.', +} satisfies Record diff --git a/packages/client/ui-settings-session-log/locale/de.json b/packages/client/ui-settings-session-log/locale/de.json new file mode 100644 index 0000000000..4fcbe1e3dc --- /dev/null +++ b/packages/client/ui-settings-session-log/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Einstellungen zum Hochladen des Sitzungsprotokolls", + "description": "Legt fest, ob Sitzungsprotokolle mit DeepSeekAnfragen des offiziellen Modells API hochgeladen werden." + } +} diff --git a/packages/client/ui-settings-session-log/locale/es.json b/packages/client/ui-settings-session-log/locale/es.json new file mode 100644 index 0000000000..58eedd3600 --- /dev/null +++ b/packages/client/ui-settings-session-log/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configuración de carga del registro de sesión", + "description": "Establece si los registros de sesión se cargan con las solicitudes del DeepSeekmodelo oficial API." + } +} diff --git a/packages/client/ui-settings-session-log/locale/fr.json b/packages/client/ui-settings-session-log/locale/fr.json new file mode 100644 index 0000000000..eaf0616333 --- /dev/null +++ b/packages/client/ui-settings-session-log/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Paramètres de téléchargement du journal de session", + "description": "Définit si les journaux de session sont téléchargés avec les requêtes DeepSeek de modèle officiel API." + } +} diff --git a/packages/client/ui-settings-session-log/locale/pt.json b/packages/client/ui-settings-session-log/locale/pt.json new file mode 100644 index 0000000000..cfe80f4cc6 --- /dev/null +++ b/packages/client/ui-settings-session-log/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurações de upload de log de sessão", + "description": "Define se os logs de sessão serão carregados com solicitações de DeepSeekmodelo oficialAPI." + } +} diff --git a/packages/client/ui-settings-session-log/locale/ru.json b/packages/client/ui-settings-session-log/locale/ru.json new file mode 100644 index 0000000000..acba5b0b17 --- /dev/null +++ b/packages/client/ui-settings-session-log/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Настройки загрузки журнала сеанса", + "description": "Определяет, загружаются ли журналы сеансов с запросами DeepSeek официальной модели API." + } +} diff --git a/packages/client/ui-settings-session-log/src/client/index.ts b/packages/client/ui-settings-session-log/src/client/index.ts index 6e749043cb..c959d1a64d 100644 --- a/packages/client/ui-settings-session-log/src/client/index.ts +++ b/packages/client/ui-settings-session-log/src/client/index.ts @@ -6,7 +6,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-layout/client' import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import { UploadPreference, type UploadSettings } from './upload-preference.ts' import { UploadRow, UploadToast, type UploadInjected } from './UploadRow.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { @@ -25,7 +25,7 @@ export const inject = ['slots', 'locale', 'configForms'] export function apply(ctx: Context): void { const locale = 'settings.sessionLog' const namespace = 'session-log-deepseek' - ctx.effect(() => ctx.locale.register(locale, { en, zh, ja })) + ctx.effect(() => ctx.locale.register(locale, { en, zh, ja, es, fr, de, ru, pt })) const form = ctx.configForms.get(namespace) const preference = new UploadPreference(form) const face = (): UploadInjected => ({ diff --git a/packages/client/ui-settings-session-log/src/client/locales.ts b/packages/client/ui-settings-session-log/src/client/locales.ts index 99b1d2f815..aeb0d22bbd 100644 --- a/packages/client/ui-settings-session-log/src/client/locales.ts +++ b/packages/client/ui-settings-session-log/src/client/locales.ts @@ -21,3 +21,43 @@ export const ja = { 'saved': '設定を保存しました', 'failed': '設定を保存できませんでした', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'Cargar registro de sesión cuando utilice el modelo oficial API', + description: 'Ayude a mejorar los modelos y productos DeepSeek.', + saved: 'Preferencia guardada', + failed: 'No se pudo guardar la preferencia', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: "Téléchargez le journal de session lors de l'utilisation du modèle officiel API", + description: 'Aidez à améliorer les modèles et produits DeepSeek.', + saved: 'Préférence enregistrée', + failed: "Impossible d'enregistrer la préférence", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'Laden Sie das Sitzungsprotokoll hoch, wenn Sie das offizielle Modell API verwenden', + description: 'Helfen Sie mit, DeepSeek-Modelle und -Produkte zu verbessern.', + saved: 'Präferenz gespeichert', + failed: 'Präferenz konnte nicht gespeichert werden', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'Загрузить журнал сеансов при использовании официальной модели API', + description: 'Помогите улучшить модели и продукты DeepSeek.', + saved: 'Предпочтение сохранено.', + failed: 'Не удалось сохранить предпочтения.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'Carregar log de sessão ao usar o modelo oficial API', + description: 'Ajude a melhorar DeepSeek modelos e produtos.', + saved: 'Preferência salva', + failed: 'Não foi possível salvar a preferência', +} satisfies Record diff --git a/packages/client/ui-settings-shell/locale/de.json b/packages/client/ui-settings-shell/locale/de.json new file mode 100644 index 0000000000..74ac0a1feb --- /dev/null +++ b/packages/client/ui-settings-shell/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Einstellungen zur Befehlsausführung", + "description": "Konfigurieren Sie das Zeitlimit für die Befehlsausführung und die Begrenzung der Ausgabelänge." + } +} diff --git a/packages/client/ui-settings-shell/locale/es.json b/packages/client/ui-settings-shell/locale/es.json new file mode 100644 index 0000000000..ea799af8ec --- /dev/null +++ b/packages/client/ui-settings-shell/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configuración de ejecución de comandos", + "description": "Configure el tiempo de espera de ejecución del comando y el límite de longitud de salida." + } +} diff --git a/packages/client/ui-settings-shell/locale/fr.json b/packages/client/ui-settings-shell/locale/fr.json new file mode 100644 index 0000000000..d1afc02893 --- /dev/null +++ b/packages/client/ui-settings-shell/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Paramètres d'exécution des commandes", + "description": "Configurez le délai d'expiration de l'exécution de la commande et la limite de longueur de sortie." + } +} diff --git a/packages/client/ui-settings-shell/locale/pt.json b/packages/client/ui-settings-shell/locale/pt.json new file mode 100644 index 0000000000..929cead7d8 --- /dev/null +++ b/packages/client/ui-settings-shell/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurações de execução de comando", + "description": "Configure o tempo limite de execução do comando e o limite de comprimento de saída." + } +} diff --git a/packages/client/ui-settings-shell/locale/ru.json b/packages/client/ui-settings-shell/locale/ru.json new file mode 100644 index 0000000000..9d95f4b7a2 --- /dev/null +++ b/packages/client/ui-settings-shell/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Настройки выполнения команд", + "description": "Настройте тайм-аут выполнения команды и ограничение длины вывода." + } +} diff --git a/packages/client/ui-settings-shell/src/client/index.ts b/packages/client/ui-settings-shell/src/client/index.ts index 55f69d6b09..a20afd04de 100644 --- a/packages/client/ui-settings-shell/src/client/index.ts +++ b/packages/client/ui-settings-shell/src/client/index.ts @@ -17,7 +17,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type { Context as ClientContext } from '@deepseek-ai/cordis' import { ShellCard } from './ShellCard.tsx' import { BASH_NS, PWSH_NS, ShellCardController } from './shell-card-controller.ts' -import { en, ja, zh, type ShellSettingsLocaleKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type ShellSettingsLocaleKey } from './locales.ts' export type { ShellCardProps } from './ShellCard.tsx' export type { ShellCardFace, ShellCardState, ShellSettings } from './shell-card-controller.ts' @@ -42,7 +42,7 @@ export const inject = ['slots', 'locale', 'configForms'] */ export function apply(ctx: ClientContext): void { const t = ctx.locale.bind(NS) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-settings-shell: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-settings-shell: dictionaries') // The base bundle composes exactly one shell executor per platform; the page binds the served one. const bash = new ShellCardController(ctx.configForms.get(BASH_NS)) const pwsh = new ShellCardController(ctx.configForms.get(PWSH_NS)) diff --git a/packages/client/ui-settings-shell/src/client/locales.ts b/packages/client/ui-settings-shell/src/client/locales.ts index 74980e77fc..2054ba8025 100644 --- a/packages/client/ui-settings-shell/src/client/locales.ts +++ b/packages/client/ui-settings-shell/src/client/locales.ts @@ -71,3 +71,93 @@ export const ja = { 'saveFailed': 'デプロイがこれらの値を受け付けませんでした。修正のため残されています。', 'invalidNumber': '数値を入力するか、空欄のままにするとデフォルトが使用されます。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'Concha', + description: 'Limite cuánto tiempo puede ejecutarse cada comando y cuánto puede generar.', + timeoutMs: 'Tiempo de espera del comando (ms)', + timeoutMsHint: 'Cuánto tiempo puede ejecutarse un comando antes de finalizar.', + maxOutputBytes: 'Límite de salida por flujo (bytes)', + maxOutputBytesHint: 'La salida más allá de esto se transfiere a un archivo temporal en lugar de perderse.', + overridden: 'Anulado', + reset: 'Restablecer los valores predeterminados', + readOnly: 'Esta implementación almacena configuraciones de solo lectura.', + unavailable: 'Este complemento no está cargado, por lo que no se puede configurar en este momento.', + save: 'Guardar', + saving: 'Guardando…', + saveFailed: 'El despliegue no aceptó estos valores; quedaron para que usted los corrigiera.', + invalidNumber: 'Ingrese un número o déjelo en blanco para usar el valor predeterminado.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: 'Coquille', + description: "Limitez la durée d'exécution de chaque commande et le volume de résultats qu'elle peut générer.", + timeoutMs: "Délai d'expiration de la commande (ms)", + timeoutMsHint: "Combien de temps une commande peut s'exécuter avant d'être terminée.", + maxOutputBytes: 'Limite de sortie par flux (octets)', + maxOutputBytesHint: "La sortie au-delà de cela se déverse dans un fichier temporaire plutôt que d'être perdue.", + overridden: 'Remplacé', + reset: 'Réinitialiser aux valeurs par défaut', + readOnly: 'Ce déploiement stocke les paramètres en lecture seule.', + unavailable: "Ce plugin n'est pas chargé, il ne peut donc pas être configuré pour le moment.", + save: 'Enregistrer', + saving: 'Sauvegarde…', + saveFailed: "Le déploiement n'a pas accepté ces valeurs ; ils vous ont été laissés à corriger.", + invalidNumber: 'Entrez un numéro ou laissez ce champ vide pour utiliser la valeur par défaut.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'Muschel', + description: 'Begrenzen Sie, wie lange jeder Befehl ausgeführt werden darf und wie viel er ausgeben darf.', + timeoutMs: 'Befehls-Timeout (ms)', + timeoutMsHint: 'Wie lange ein Befehl ausgeführt werden darf, bevor er beendet wird.', + maxOutputBytes: 'Ausgabeobergrenze pro Stream (Byte)', + maxOutputBytesHint: 'Darüber hinausgehende Ausgaben werden in eine temporäre Datei übertragen und gehen nicht verloren.', + overridden: 'Überschrieben', + reset: 'Auf Standard zurücksetzen', + readOnly: 'Diese Bereitstellung speichert Einstellungen schreibgeschützt.', + unavailable: 'Dieses Plugin ist nicht geladen und kann daher derzeit nicht konfiguriert werden.', + save: 'Speichern', + saving: 'Speichern…', + saveFailed: 'Die Bereitstellung hat diese Werte nicht akzeptiert. Sie mussten sie korrigieren.', + invalidNumber: 'Geben Sie eine Zahl ein oder lassen Sie das Feld leer, um den Standardwert zu verwenden.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'Ракушка', + description: 'Ограничьте продолжительность выполнения каждой команды и объем вывода.', + timeoutMs: 'Таймаут команды (мс)', + timeoutMsHint: 'Как долго может выполняться одна команда до ее завершения.', + maxOutputBytes: 'Ограничение вывода на поток (байты)', + maxOutputBytesHint: 'Вывод, выходящий за пределы этого, сохраняется во временный файл, а не теряется.', + overridden: 'Переопределено', + reset: 'Сбросить настройки по умолчанию', + readOnly: 'В этом развертывании параметры сохраняются только для чтения.', + unavailable: 'Этот плагин не загружен, поэтому его невозможно настроить прямо сейчас.', + save: 'Сохранить', + saving: 'Сохранение…', + saveFailed: 'Развертывание не приняло эти значения; их оставили вам на исправление.', + invalidNumber: 'Введите число или оставьте поле пустым, чтобы использовать значение по умолчанию.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'Concha', + description: 'Limite quanto tempo cada comando pode ser executado e quanto ele pode gerar.', + timeoutMs: 'Tempo limite do comando (ms)', + timeoutMsHint: 'Quanto tempo um comando pode ser executado antes de ser encerrado.', + maxOutputBytes: 'Limite de saída por stream (bytes)', + maxOutputBytesHint: 'A saída além disso é transferida para um arquivo temporário em vez de ser perdida.', + overridden: 'Substituído', + reset: 'Redefinir para o padrão', + readOnly: 'Esta implantação armazena configurações somente leitura.', + unavailable: 'Este plugin não está carregado, portanto não pode ser configurado agora.', + save: 'Salvar', + saving: 'Salvando…', + saveFailed: 'A implantação não aceitou esses valores; eles foram deixados para você corrigir.', + invalidNumber: 'Insira um número ou deixe em branco para usar o padrão.', +} satisfies Record diff --git a/packages/client/ui-settings-subagent/locale/de.json b/packages/client/ui-settings-subagent/locale/de.json new file mode 100644 index 0000000000..ecce9cc69e --- /dev/null +++ b/packages/client/ui-settings-subagent/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Subagenteneinstellungen", + "description": "Konfigurieren Sie die Delegationstiefe, die Anzahl der Parallelitäten und optionale Modelle." + } +} diff --git a/packages/client/ui-settings-subagent/locale/es.json b/packages/client/ui-settings-subagent/locale/es.json new file mode 100644 index 0000000000..3323c6f124 --- /dev/null +++ b/packages/client/ui-settings-subagent/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configuración de subagente", + "description": "Configure la profundidad de la delegación, el número de simultaneidades y modelos opcionales." + } +} diff --git a/packages/client/ui-settings-subagent/locale/fr.json b/packages/client/ui-settings-subagent/locale/fr.json new file mode 100644 index 0000000000..aec77b780f --- /dev/null +++ b/packages/client/ui-settings-subagent/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Paramètres du sous-agent", + "description": "Configurez la profondeur de la délégation, le nombre de simultanéités et les modèles facultatifs." + } +} diff --git a/packages/client/ui-settings-subagent/locale/pt.json b/packages/client/ui-settings-subagent/locale/pt.json new file mode 100644 index 0000000000..00b8568382 --- /dev/null +++ b/packages/client/ui-settings-subagent/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurações do subagente", + "description": "Configure a profundidade da delegação, o número de simultaneidades e os modelos opcionais." + } +} diff --git a/packages/client/ui-settings-subagent/locale/ru.json b/packages/client/ui-settings-subagent/locale/ru.json new file mode 100644 index 0000000000..7c74680dd0 --- /dev/null +++ b/packages/client/ui-settings-subagent/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Настройки субагента", + "description": "Настройте глубину делегирования, количество параллелизма и дополнительные модели." + } +} diff --git a/packages/client/ui-settings-subagent/src/client/index.ts b/packages/client/ui-settings-subagent/src/client/index.ts index 42811181ee..107e206b5b 100644 --- a/packages/client/ui-settings-subagent/src/client/index.ts +++ b/packages/client/ui-settings-subagent/src/client/index.ts @@ -23,7 +23,7 @@ import { SubagentLimitsCardController } from './subagent-limits-card-controller. import { SUBAGENT_MODEL_SELECTION_NS, SubagentModelSelectionCardController, } from './subagent-model-selection-card-controller.ts' -import { en, ja, zh, type SubagentSettingsLocaleKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type SubagentSettingsLocaleKey } from './locales.ts' export type { SubagentCardProps } from './SubagentCard.tsx' export type { SubagentCardFace } from './subagent-card-controller.ts' @@ -58,7 +58,7 @@ export const inject = ['slots', 'locale', 'remote', 'remote.session', 'configFor */ export function apply(ctx: ClientContext): void { const t = ctx.locale.bind(NS) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-settings-subagent: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-settings-subagent: dictionaries') const limits = new SubagentLimitsCardController(ctx.configForms.get(SUBAGENT_NS)) ctx.effect(() => () => { limits.dispose() }, 'ui-settings-subagent: limits form subscription') const models = new SubagentModelSelectionCardController( diff --git a/packages/client/ui-settings-subagent/src/client/locales.ts b/packages/client/ui-settings-subagent/src/client/locales.ts index d92185f3dd..6a575814d8 100644 --- a/packages/client/ui-settings-subagent/src/client/locales.ts +++ b/packages/client/ui-settings-subagent/src/client/locales.ts @@ -146,3 +146,198 @@ export const ja = { 'subagentModelSelectionConflict': '設定が別の場所で変更されました。下書きを破棄して再試行してください。', 'subagentModelSelectionOff': 'サブエージェントは設定済みのデフォルトを使用するか、親エージェントのモデルを継承します。保存済みのモデル選択は保持されます。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + overridden: 'Anulado', + reset: 'Restablecer los valores predeterminados', + readOnly: 'Esta implementación almacena configuraciones de solo lectura.', + unavailable: 'Este complemento no está cargado, por lo que no se puede configurar en este momento.', + save: 'Guardar', + saving: 'Guardando…', + saveFailed: 'El despliegue no aceptó estos valores; quedaron para que usted los corrigiera.', + subagentTitle: 'Subagente', + subagentDescription: 'Establezca la profundidad, el recuento y los modelos de recursividad del subagente.', + subagentLimitsTitle: 'Límites', + subagentMaxDepth: 'Profundidad máxima de recursividad', + subagentDepthHelpLabel: 'Acerca de la profundidad máxima de recursividad', + subagentDepthHelp: 'Limita cuántos niveles de Subagentes puede crear un Agente.', + subagentDepthZero: 'Deshabilitar subagentes', + subagentDepthOne: 'Sólo el Agente principal puede crear Subagentes', + subagentDepthOverride: 'Si una herramienta define su propia profundidad de recursividad máxima, esa configuración tiene prioridad.', + subagentMaxActive: 'Límite de paralelismo de subagente', + subagentCapacityHelpLabel: 'Acerca del límite de paralelismo del subagente', + subagentCapacityHelp: 'Total de subagentes activos bajo el mismo agente principal, en todos los niveles de recursividad. El Agente principal está excluido. Las nuevas solicitudes de inicio se rechazan cuando se alcanza el límite.', + subagentDepthInvalid: 'Introduzca un número entero de 0 o más.', + subagentCapacityInvalid: 'Introduzca un número entero de 1 o más.', + subagentModelSelectionTitle: 'Selección de modelo', + subagentModelSelectionToggle: 'Permitir a los agentes elegir modelos para Subagentes', + subagentModelSelectionChoose: 'Cuando está habilitado, los agentes pueden elegir un proveedor, modelo y esfuerzo de razonamiento para cada subagente entre los modelos autorizados a continuación. Aplica sólo para sesiones nuevas.', + subagentModelSelectionAllowed: 'Los agentes modelos pueden elegir', + subagentModelSelectionLoading: 'Cargando modelos…', + subagentModelSelectionLoadFailed: 'No se pudieron cargar los modelos.', + subagentModelSelectionRetry: 'Reintentar', + subagentModelSelectionPartial: 'No se pudieron cargar algunos proveedores de modelos; Las opciones guardadas siguen siendo extraíbles.', + subagentModelSelectionUnavailable: 'Actualmente no disponible', + subagentModelSelectionUnavailableGroup: 'Guardado pero actualmente no disponible', + subagentModelSelectionEmpty: 'Ningún proveedor de modelos anuncia actualmente un modelo.', + subagentModelSelectionRequired: 'Seleccione al menos un modelo antes de guardar.', + subagentModelSelectionConflict: 'La configuración cambió en otros lugares. Descarta tu borrador y vuelve a intentarlo.', + subagentModelSelectionOff: 'Los subagentes utilizan valores predeterminados configurados o heredan el modelo del agente principal. Las opciones de modelo guardadas se conservan.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + overridden: 'Remplacé', + reset: 'Réinitialiser aux valeurs par défaut', + readOnly: 'Ce déploiement stocke les paramètres en lecture seule.', + unavailable: "Ce plugin n'est pas chargé, il ne peut donc pas être configuré pour le moment.", + save: 'Enregistrer', + saving: 'Sauvegarde…', + saveFailed: "Le déploiement n'a pas accepté ces valeurs ; ils vous ont été laissés à corriger.", + subagentTitle: 'Sous-agent', + subagentDescription: 'Définissez la profondeur, le nombre et les modèles de récursion du sous-agent.', + subagentLimitsTitle: 'Limites', + subagentMaxDepth: 'Profondeur de récursion maximale', + subagentDepthHelpLabel: 'À propos de la profondeur de récursion maximale', + subagentDepthHelp: "Limite le nombre de niveaux de sous-agents qu'un agent peut créer.", + subagentDepthZero: 'Désactiver les sous-agents', + subagentDepthOne: "Seul l'agent principal peut créer des sous-agents", + subagentDepthOverride: 'Si un outil définit sa propre profondeur de récursion maximale, ce paramètre est prioritaire.', + subagentMaxActive: 'Limite de parallélisme des sous-agents', + subagentCapacityHelpLabel: 'À propos de la limite de parallélisme des sous-agents', + subagentCapacityHelp: "Total des sous-agents actifs sous le même agent principal, à tous les niveaux de récursion. L'Agent principal est exclu. Les nouvelles demandes de démarrage sont rejetées lorsque la limite est atteinte.", + subagentDepthInvalid: 'Entrez un nombre entier de 0 ou plus.', + subagentCapacityInvalid: 'Entrez un nombre entier de 1 ou plus.', + subagentModelSelectionTitle: 'Sélection du modèle', + subagentModelSelectionToggle: 'Autoriser les agents à choisir des modèles pour les sous-agents', + subagentModelSelectionChoose: "Lorsque cette option est activée, les agents peuvent choisir un fournisseur, un modèle et un effort de raisonnement pour chaque sous-agent parmi les modèles autorisés ci-dessous. S'applique uniquement aux nouvelles sessions.", + subagentModelSelectionAllowed: 'Les agents modèles peuvent choisir', + subagentModelSelectionLoading: 'Chargement des modèles…', + subagentModelSelectionLoadFailed: "Les modèles n'ont pas pu être chargés.", + subagentModelSelectionRetry: 'Réessayer', + subagentModelSelectionPartial: "Certains fournisseurs de modèles n'ont pas pu être chargés ; les choix enregistrés restent amovibles.", + subagentModelSelectionUnavailable: 'Actuellement indisponible', + subagentModelSelectionUnavailableGroup: 'Enregistré mais actuellement indisponible', + subagentModelSelectionEmpty: "Aucun fournisseur de modèles n'annonce actuellement un modèle.", + subagentModelSelectionRequired: 'Sélectionnez au moins un modèle avant de sauvegarder.', + subagentModelSelectionConflict: 'Paramètres modifiés ailleurs. Supprimez votre brouillon et réessayez.', + subagentModelSelectionOff: "Les sous-agents utilisent les valeurs par défaut configurées ou héritent du modèle de l'agent parent. Les choix de modèles enregistrés sont conservés.", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + overridden: 'Überschrieben', + reset: 'Auf Standard zurücksetzen', + readOnly: 'Diese Bereitstellung speichert Einstellungen schreibgeschützt.', + unavailable: 'Dieses Plugin ist nicht geladen und kann daher derzeit nicht konfiguriert werden.', + save: 'Speichern', + saving: 'Speichern…', + saveFailed: 'Die Bereitstellung hat diese Werte nicht akzeptiert. Sie mussten sie korrigieren.', + subagentTitle: 'Subagent', + subagentDescription: 'Legen Sie die Rekursionstiefe, Anzahl und Modelle des Subagenten fest.', + subagentLimitsTitle: 'Grenzen', + subagentMaxDepth: 'Maximale Rekursionstiefe', + subagentDepthHelpLabel: 'Über maximale Rekursionstiefe', + subagentDepthHelp: 'Begrenzt die Anzahl der Ebenen von Subagenten, die ein Agent erstellen kann.', + subagentDepthZero: 'Subagenten deaktivieren', + subagentDepthOne: 'Nur der Hauptagent kann Unteragenten erstellen', + subagentDepthOverride: 'Wenn ein Werkzeug seine eigene maximale Rekursionstiefe definiert, hat diese Einstellung Vorrang.', + subagentMaxActive: 'Grenzwert für die Parallelität des Subagenten', + subagentCapacityHelpLabel: 'Informationen zum Subagenten-Parallelitätslimit', + subagentCapacityHelp: 'Gesamtzahl der Live-Subagenten unter demselben Hauptagenten, über alle Rekursionsebenen hinweg. Der Hauptvertreter ist ausgeschlossen. Neue Startanfragen werden abgelehnt, wenn das Limit erreicht ist.', + subagentDepthInvalid: 'Geben Sie eine ganze Zahl von 0 oder mehr ein.', + subagentCapacityInvalid: 'Geben Sie eine ganze Zahl von 1 oder mehr ein.', + subagentModelSelectionTitle: 'Modellauswahl', + subagentModelSelectionToggle: 'Erlauben Sie Agenten, Modelle für Subagenten auszuwählen', + subagentModelSelectionChoose: 'Wenn diese Option aktiviert ist, können Agenten aus den unten aufgeführten autorisierten Modellen einen Anbieter, ein Modell und einen Argumentationsaufwand für jeden Subagenten auswählen. Gilt nur für neue Sitzungen.', + subagentModelSelectionAllowed: 'Modelagenten können wählen', + subagentModelSelectionLoading: 'Modelle werden geladen…', + subagentModelSelectionLoadFailed: 'Modelle konnten nicht geladen werden.', + subagentModelSelectionRetry: 'Erneut versuchen', + subagentModelSelectionPartial: 'Einige Modellanbieter konnten nicht geladen werden; Gespeicherte Auswahlmöglichkeiten bleiben entfernbar.', + subagentModelSelectionUnavailable: 'Derzeit nicht verfügbar', + subagentModelSelectionUnavailableGroup: 'Gespeichert, aber derzeit nicht verfügbar', + subagentModelSelectionEmpty: 'Derzeit bewirbt kein Modelanbieter ein Model.', + subagentModelSelectionRequired: 'Wählen Sie vor dem Speichern mindestens ein Modell aus.', + subagentModelSelectionConflict: 'Einstellungen wurden an anderer Stelle geändert. Verwerfen Sie Ihren Entwurf und versuchen Sie es erneut.', + subagentModelSelectionOff: 'Subagenten verwenden konfigurierte Standardwerte oder erben das Modell des übergeordneten Agenten. Gespeicherte Modellauswahlen bleiben erhalten.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + overridden: 'Переопределено', + reset: 'Сбросить настройки по умолчанию', + readOnly: 'В этом развертывании параметры сохраняются только для чтения.', + unavailable: 'Этот плагин не загружен, поэтому его невозможно настроить прямо сейчас.', + save: 'Сохранить', + saving: 'Сохранение…', + saveFailed: 'Развертывание не приняло эти значения; их оставили вам на исправление.', + subagentTitle: 'Субагент', + subagentDescription: 'Установите глубину рекурсии субагента, количество и модели.', + subagentLimitsTitle: 'Пределы', + subagentMaxDepth: 'Максимальная глубина рекурсии', + subagentDepthHelpLabel: 'О максимальной глубине рекурсии', + subagentDepthHelp: 'Ограничивает количество уровней субагентов, которые может создать агент.', + subagentDepthZero: 'Отключить субагенты', + subagentDepthOne: 'Только главный агент может создавать субагентов.', + subagentDepthOverride: 'Если инструмент определяет собственную максимальную глубину рекурсии, этот параметр имеет приоритет.', + subagentMaxActive: 'Предел параллелизма субагентов', + subagentCapacityHelpLabel: 'О пределе параллелизма субагентов', + subagentCapacityHelp: 'Общее количество действующих субагентов под одним и тем же основным агентом на всех уровнях рекурсии. Главный Агент исключен. Новые запросы на запуск отклоняются при достижении предела.', + subagentDepthInvalid: 'Введите целое число от 0 или более.', + subagentCapacityInvalid: 'Введите целое число от 1 или более.', + subagentModelSelectionTitle: 'Выбор модели', + subagentModelSelectionToggle: 'Разрешить агентам выбирать модели для субагентов', + subagentModelSelectionChoose: 'Если этот параметр включен, агенты могут выбирать поставщика, модель и усилия по обоснованию для каждого субагента из авторизованных моделей, указанных ниже. Применяется только к новым сеансам.', + subagentModelSelectionAllowed: 'Модели агенты могут выбирать', + subagentModelSelectionLoading: 'Загрузка моделей…', + subagentModelSelectionLoadFailed: 'Не удалось загрузить модели.', + subagentModelSelectionRetry: 'Повторить', + subagentModelSelectionPartial: 'Некоторые поставщики моделей не удалось загрузить; сохраненные варианты остаются съемными.', + subagentModelSelectionUnavailable: 'В настоящее время недоступен', + subagentModelSelectionUnavailableGroup: 'Сохранено, но сейчас недоступно', + subagentModelSelectionEmpty: 'Ни один поставщик моделей в настоящее время не рекламирует модель.', + subagentModelSelectionRequired: 'Прежде чем сохранить, выберите хотя бы одну модель.', + subagentModelSelectionConflict: 'Настройки изменены в другом месте. Отмените черновик и повторите попытку.', + subagentModelSelectionOff: 'Субагенты используют настроенные значения по умолчанию или наследуют модель родительского агента. Сохраненные варианты моделей сохраняются.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + overridden: 'Substituído', + reset: 'Redefinir para o padrão', + readOnly: 'Esta implantação armazena configurações somente leitura.', + unavailable: 'Este plugin não está carregado, portanto não pode ser configurado agora.', + save: 'Salvar', + saving: 'Salvando…', + saveFailed: 'A implantação não aceitou esses valores; eles foram deixados para você corrigir.', + subagentTitle: 'Subagente', + subagentDescription: 'Defina a profundidade, a contagem e os modelos de recursão do subagente.', + subagentLimitsTitle: 'Limites', + subagentMaxDepth: 'Profundidade máxima de recursão', + subagentDepthHelpLabel: 'Sobre a profundidade máxima de recursão', + subagentDepthHelp: 'Limita quantos níveis de Subagentes um Agente pode criar.', + subagentDepthZero: 'Desabilitar subagentes', + subagentDepthOne: 'Somente o Agente principal pode criar Subagentes', + subagentDepthOverride: 'Se uma ferramenta definir sua própria profundidade máxima de recursão, essa configuração terá precedência.', + subagentMaxActive: 'Limite de paralelismo de subagentes', + subagentCapacityHelpLabel: 'Sobre o limite de paralelismo do subagente', + subagentCapacityHelp: 'Total de subagentes ativos sob o mesmo agente principal, em todos os níveis de recursão. O Agente principal é excluído. Novas solicitações de início serão rejeitadas quando o limite for atingido.', + subagentDepthInvalid: 'Insira um número inteiro de 0 ou mais.', + subagentCapacityInvalid: 'Insira um número inteiro de 1 ou mais.', + subagentModelSelectionTitle: 'Seleção de modelo', + subagentModelSelectionToggle: 'Permitir que os agentes escolham modelos para subagentes', + subagentModelSelectionChoose: 'Quando ativado, os agentes podem escolher um provedor, um modelo e um esforço de raciocínio para cada subagente dentre os modelos autorizados abaixo. Aplica-se apenas a novas sessões.', + subagentModelSelectionAllowed: 'Os agentes de modelos podem escolher', + subagentModelSelectionLoading: 'Carregando modelos…', + subagentModelSelectionLoadFailed: 'Não foi possível carregar os modelos.', + subagentModelSelectionRetry: 'Tentar novamente', + subagentModelSelectionPartial: 'Alguns provedores de modelo não puderam ser carregados; as escolhas salvas permanecem removíveis.', + subagentModelSelectionUnavailable: 'Atualmente indisponível', + subagentModelSelectionUnavailableGroup: 'Salvo, mas atualmente indisponível', + subagentModelSelectionEmpty: 'Atualmente, nenhum fornecedor de modelos anuncia um modelo.', + subagentModelSelectionRequired: 'Selecione pelo menos um modelo antes de salvar.', + subagentModelSelectionConflict: 'Configurações alteradas em outro lugar. Descarte seu rascunho e tente novamente.', + subagentModelSelectionOff: 'Os subagentes usam padrões configurados ou herdam o modelo do agente pai. As opções de modelo salvas são mantidas.', +} satisfies Record diff --git a/packages/client/ui-settings-web-search/locale/de.json b/packages/client/ui-settings-web-search/locale/de.json new file mode 100644 index 0000000000..5aa238488d --- /dev/null +++ b/packages/client/ui-settings-web-search/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Einstellungen für die Websuche", + "description": "Konfigurieren Sie den Schlüssel, die Dienstadresse und das Anforderungsbudget für die DeepSeek-Websuche." + } +} diff --git a/packages/client/ui-settings-web-search/locale/es.json b/packages/client/ui-settings-web-search/locale/es.json new file mode 100644 index 0000000000..1e1d3f5e7b --- /dev/null +++ b/packages/client/ui-settings-web-search/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configuración de búsqueda web", + "description": "Configure la clave, la dirección del servicio y solicite el presupuesto para la búsqueda web DeepSeek." + } +} diff --git a/packages/client/ui-settings-web-search/locale/fr.json b/packages/client/ui-settings-web-search/locale/fr.json new file mode 100644 index 0000000000..6c20e4212a --- /dev/null +++ b/packages/client/ui-settings-web-search/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Paramètres de recherche sur le Web", + "description": "Configurez la clé, l'adresse du service et le budget de demande pour la recherche sur le Web DeepSeek." + } +} diff --git a/packages/client/ui-settings-web-search/locale/pt.json b/packages/client/ui-settings-web-search/locale/pt.json new file mode 100644 index 0000000000..ce7d68d86c --- /dev/null +++ b/packages/client/ui-settings-web-search/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Configurações de pesquisa na web", + "description": "Configure a chave, o endereço do serviço e solicite o orçamento para DeepSeek pesquisa na web." + } +} diff --git a/packages/client/ui-settings-web-search/locale/ru.json b/packages/client/ui-settings-web-search/locale/ru.json new file mode 100644 index 0000000000..b8bd1cd774 --- /dev/null +++ b/packages/client/ui-settings-web-search/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Настройки веб-поиска", + "description": "Настройте ключ, адрес службы и бюджет запроса для веб-поиска DeepSeek." + } +} diff --git a/packages/client/ui-settings-web-search/src/client/index.ts b/packages/client/ui-settings-web-search/src/client/index.ts index 461237a165..b4ed1c6779 100644 --- a/packages/client/ui-settings-web-search/src/client/index.ts +++ b/packages/client/ui-settings-web-search/src/client/index.ts @@ -22,7 +22,7 @@ import type {} from '@deepseek-ai/dsh-api-remotes/client' import type { Context as ClientContext } from '@deepseek-ai/cordis' import { WebSearchCard } from './WebSearchCard.tsx' import { WEB_API_NS, WEB_NS, WEB_SEARCH_NS, WebSearchCardController } from './web-search-card-controller.ts' -import { en, ja, zh, type WebSearchSettingsLocaleKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type WebSearchSettingsLocaleKey } from './locales.ts' export type { WebSearchCardProps } from './WebSearchCard.tsx' export type { @@ -49,7 +49,7 @@ export const inject = ['slots', 'locale', 'remote', 'remote.credentials', 'confi */ export function apply(ctx: ClientContext): void { const t = ctx.locale.bind(NS) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-settings-web-search: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-settings-web-search: dictionaries') const card = new WebSearchCardController({ web: ctx.configForms.get(WEB_NS), messages: ctx.configForms.get(WEB_SEARCH_NS), api: ctx.configForms.get(WEB_API_NS), }, ctx) diff --git a/packages/client/ui-settings-web-search/src/client/locales.ts b/packages/client/ui-settings-web-search/src/client/locales.ts index 65d21ae8f0..1290fb9000 100644 --- a/packages/client/ui-settings-web-search/src/client/locales.ts +++ b/packages/client/ui-settings-web-search/src/client/locales.ts @@ -111,3 +111,158 @@ export const ja = { 'invalidNumber': '数値を入力するか、デフォルトを使用するには空欄にしてください。', 'invalidCount': '1〜50 の整数を入力するか、デフォルトを使用するには空欄にしてください。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'búsqueda web', + description: 'Configure el proveedor de búsqueda DeepSeek.', + useSearchApi: 'Utilice la nueva búsqueda web API', + useSearchApiHint: 'Desactivado de forma predeterminada: la búsqueda se ejecuta a través de Mensajes DeepSeek. Para conocer las características y los precios del nuevo Web Search API, consulte el ', + searchGuide: 'Guía de búsqueda y recuperación', + searchGuideUrl: 'https://api-docs.deepseek.com/guides/search', + apiKey: 'Clave de API', + apiKeyHint: 'Almacenado fuera del archivo de configuración. Déjelo en blanco para conservar la clave actual.', + apiKeySet: 'Se configura una clave.', + apiKeyUnset: 'No hay ninguna clave configurada; solo se pueden buscar conversaciones que utilicen un modelo de cuenta DeepSeek, a través del punto final predeterminado.', + baseUrl: 'Mensajes API URL base', + baseUrlHint: 'Incluya el prefijo API, como https://api.deepseek.com/anthropic/v1.. El proveedor agrega /messages. Déjelo en blanco para restaurar el valor predeterminado.', + searchApiBaseUrl: 'Buscar/obtener URL base API', + searchApiBaseUrlHint: 'Incluya el prefijo API, como https://api.deepseek.com/v1/tools.. El proveedor agrega /search o /fetch. Déjelo en blanco para restaurar el valor predeterminado.', + maxUses: 'Búsquedas máximas por solicitud', + maxUsesHint: 'Cuántas búsquedas puede ejecutar el modelo auxiliar dentro de una solicitud de Mensajes. Aplica sólo para Mensajes API.', + count: 'Resultados por búsqueda', + countHint: 'Resultados devueltos por consulta: un número entero del 1 al 50, valor predeterminado 10. Esto controla el recuento de resultados, no las llamadas de búsqueda. web_search no guarda más fuentes que su propio límite de resultados.', + overridden: 'Anulado', + reset: 'Restablecer los valores predeterminados', + readOnly: 'Esta implementación almacena configuraciones de solo lectura.', + unavailable: 'Este complemento no está cargado, por lo que no se puede configurar en este momento.', + save: 'Guardar', + saving: 'Guardando…', + saveFailed: 'El despliegue no aceptó estos valores; quedaron para que usted los corrigiera.', + invalidNumber: 'Ingrese un número o déjelo en blanco para usar el valor predeterminado.', + invalidCount: 'Ingrese un número entero del 1 al 50 o déjelo en blanco para usar el valor predeterminado.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: 'Recherche sur le Web', + description: 'Configurez le moteur de recherche DeepSeek.', + useSearchApi: 'Utilisez la nouvelle recherche Web API', + useSearchApiHint: "Désactivé par défaut : la recherche s'effectue dans les messages DeepSeek. Pour connaître les fonctionnalités et les tarifs du nouveau Web Search API, consultez le ", + searchGuide: 'Guide de recherche et de récupération', + searchGuideUrl: 'https://api-docs.deepseek.com/guides/search', + apiKey: 'Clé API', + apiKeyHint: 'Stocké en dehors du fichier de paramètres. Laissez vide pour conserver la clé actuelle.', + apiKeySet: 'Une clé est configurée.', + apiKeyUnset: "Aucune clé n'est configurée ; seules les conversations utilisant un modèle de compte DeepSeek peuvent effectuer une recherche, via le point de terminaison par défaut.", + baseUrl: 'URL de base des messages API', + baseUrlHint: 'Incluez le préfixe API, tel que https://api.deepseek.com/anthropic/v1.. Le fournisseur ajoute /messages. Laissez vide pour restaurer la valeur par défaut.', + searchApiBaseUrl: "Rechercher/récupérer l'URL de base de API", + searchApiBaseUrlHint: 'Incluez le préfixe API, tel que https://api.deepseek.com/v1/tools.. Le fournisseur ajoute /search ou /fetch. Laissez vide pour restaurer la valeur par défaut.', + maxUses: 'Nombre maximum de recherches par requête', + maxUsesHint: "Combien de recherches le modèle auxiliaire peut exécuter dans une requête de messages. S'applique uniquement aux messages API.", + count: 'Résultats par recherche', + countHint: 'Résultats renvoyés par requête : un nombre entier de 1 à 50, 10 par défaut. Cela contrôle le nombre de résultats, pas les appels de recherche. web_search ne conserve pas plus de sources que sa propre limite de résultats.', + overridden: 'Remplacé', + reset: 'Réinitialiser aux valeurs par défaut', + readOnly: 'Ce déploiement stocke les paramètres en lecture seule.', + unavailable: "Ce plugin n'est pas chargé, il ne peut donc pas être configuré pour le moment.", + save: 'Enregistrer', + saving: 'Sauvegarde…', + saveFailed: "Le déploiement n'a pas accepté ces valeurs ; ils vous ont été laissés à corriger.", + invalidNumber: 'Entrez un numéro ou laissez ce champ vide pour utiliser la valeur par défaut.', + invalidCount: 'Entrez un nombre entier compris entre 1 et 50 ou laissez vide pour utiliser la valeur par défaut.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'Websuche', + description: 'Richten Sie den Suchanbieter DeepSeek ein.', + useSearchApi: 'Nutzen Sie die neue Websuche API', + useSearchApiHint: 'Standardmäßig deaktiviert: Die Suche erfolgt über DeepSeek-Nachrichten. Informationen zu den Funktionen und Preisen der neuen Websuche API finden Sie im ', + searchGuide: 'Leitfaden zum Suchen und Abrufen', + searchGuideUrl: 'https://api-docs.deepseek.com/guides/search', + apiKey: 'API-Schlüssel', + apiKeyHint: 'Außerhalb der Einstellungsdatei gespeichert. Lassen Sie das Feld leer, um den aktuellen Schlüssel beizubehalten.', + apiKeySet: 'Ein Schlüssel ist konfiguriert.', + apiKeyUnset: 'Es ist kein Schlüssel konfiguriert; Nur Konversationen, die ein DeepSeek-Kontomodell verwenden, können über den Standardendpunkt suchen.', + baseUrl: 'Nachrichten API Basis-URL', + baseUrlHint: 'Fügen Sie das Präfix API ein, z. B. https://api.deepseek.com/anthropic/v1.. Der Anbieter hängt /messages an. Lassen Sie das Feld leer, um die Standardeinstellung wiederherzustellen.', + searchApiBaseUrl: 'Suchen/Abrufen der API-Basis-URL', + searchApiBaseUrlHint: 'Fügen Sie das Präfix API ein, z. B. https://api.deepseek.com/v1/tools.. Der Anbieter hängt /search oder /fetch an. Lassen Sie das Feld leer, um die Standardeinstellung wiederherzustellen.', + maxUses: 'Maximale Anzahl an Suchanfragen pro Anfrage', + maxUsesHint: 'Wie viele Suchvorgänge das Hilfsmodell innerhalb einer Nachrichtenanforderung ausführen darf. Gilt nur für die Nachrichten API.', + count: 'Ergebnisse pro Suche', + countHint: 'Pro Abfrage zurückgegebene Ergebnisse: eine Ganzzahl von 1 bis 50, Standard 10. Dies steuert die Ergebnisanzahl, nicht Suchaufrufe. web_search behält nicht mehr Quellen als sein eigenes Ergebnislimit.', + overridden: 'Überschrieben', + reset: 'Auf Standard zurücksetzen', + readOnly: 'Diese Bereitstellung speichert Einstellungen schreibgeschützt.', + unavailable: 'Dieses Plugin ist nicht geladen und kann daher derzeit nicht konfiguriert werden.', + save: 'Speichern', + saving: 'Speichern…', + saveFailed: 'Die Bereitstellung hat diese Werte nicht akzeptiert. Sie mussten sie korrigieren.', + invalidNumber: 'Geben Sie eine Zahl ein oder lassen Sie das Feld leer, um den Standardwert zu verwenden.', + invalidCount: 'Geben Sie eine Ganzzahl zwischen 1 und 50 ein oder lassen Sie das Feld leer, um den Standardwert zu verwenden.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'Веб-поиск', + description: 'Настройте поисковую систему DeepSeek.', + useSearchApi: 'Используйте новый веб-поиск API', + useSearchApiHint: 'По умолчанию отключено: поиск осуществляется по сообщениям DeepSeek. Информацию о новых функциях и ценах веб-поиска API см. ', + searchGuide: 'Руководство по поиску и получению', + searchGuideUrl: 'https://api-docs.deepseek.com/guides/search', + apiKey: 'Ключ API', + apiKeyHint: 'Хранится вне файла настроек. Оставьте пустым, чтобы сохранить текущий ключ.', + apiKeySet: 'Ключ настроен.', + apiKeyUnset: 'Ключ не настроен; только разговоры, использующие модель учетной записи DeepSeek, могут выполнять поиск через конечную точку по умолчанию.', + baseUrl: 'Базовый URL-адрес сообщений API', + baseUrlHint: 'Включите префикс API, например https://api.deepseek.com/anthropic/v1.. Поставщик добавляет /messages. Оставьте пустым, чтобы восстановить значение по умолчанию.', + searchApiBaseUrl: 'Найдите/получите базовый URL-адрес API', + searchApiBaseUrlHint: 'Включите префикс API, например https://api.deepseek.com/v1/tools.. Поставщик добавляет /search или /fetch. Оставьте пустым, чтобы восстановить значение по умолчанию.', + maxUses: 'Максимальное количество запросов на запрос', + maxUsesHint: 'Сколько поисков вспомогательная модель может выполнить в рамках одного запроса сообщений. Применяется только к сообщениям API.', + count: 'Результаты по поиску', + countHint: 'Результаты, возвращаемые по запросу: целое число от 1 до 50, по умолчанию 10. Это контролирует количество результатов, а не поисковые вызовы. web_search хранит не больше источников, чем его собственный лимит результатов.', + overridden: 'Переопределено', + reset: 'Сбросить настройки по умолчанию', + readOnly: 'В этом развертывании параметры сохраняются только для чтения.', + unavailable: 'Этот плагин не загружен, поэтому его невозможно настроить прямо сейчас.', + save: 'Сохранить', + saving: 'Сохранение…', + saveFailed: 'Развертывание не приняло эти значения; их оставили вам на исправление.', + invalidNumber: 'Введите число или оставьте поле пустым, чтобы использовать значение по умолчанию.', + invalidCount: 'Введите целое число от 1 до 50 или оставьте поле пустым, чтобы использовать значение по умолчанию.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'Pesquisa na web', + description: 'Configure o provedor de pesquisa DeepSeek.', + useSearchApi: 'Use a nova Pesquisa na Web API', + useSearchApiHint: 'Desativado por padrão: a pesquisa é executada em DeepSeek Mensagens. Para conhecer os recursos e preços da nova Pesquisa na Web API, consulte o ', + searchGuide: 'Guia de pesquisa e busca', + searchGuideUrl: 'https://api-docs.deepseek.com/guides/search', + apiKey: 'API chave', + apiKeyHint: 'Armazenado fora do arquivo de configurações. Deixe em branco para manter a chave atual.', + apiKeySet: 'Uma chave está configurada.', + apiKeyUnset: 'Nenhuma chave está configurada; somente conversas que usam um modelo de conta DeepSeek podem pesquisar, por meio do endpoint padrão.', + baseUrl: 'URL base de mensagens API', + baseUrlHint: 'Inclua o prefixo API, como https://api.deepseek.com/anthropic/v1. O provedor anexa /messages. Deixe em branco para restaurar o padrão.', + searchApiBaseUrl: 'Pesquisar/buscar API URL base', + searchApiBaseUrlHint: 'Inclua o prefixo API, como https://api.deepseek.com/v1/tools. O provedor anexa /search ou /fetch. Deixe em branco para restaurar o padrão.', + maxUses: 'Máximo de pesquisas por solicitação', + maxUsesHint: 'Quantas pesquisas o modelo auxiliar pode executar em uma solicitação de Mensagens. Aplica-se somente às Mensagens API.', + count: 'Resultados por pesquisa', + countHint: 'Resultados retornados por consulta: um número inteiro de 1 a 50, padrão 10. Isso controla a contagem de resultados, não as chamadas de pesquisa. web_search não mantém mais fontes do que seu próprio limite de resultados.', + overridden: 'Substituído', + reset: 'Redefinir para o padrão', + readOnly: 'Esta implantação armazena configurações somente leitura.', + unavailable: 'Este plugin não está carregado, portanto não pode ser configurado agora.', + save: 'Salvar', + saving: 'Salvando…', + saveFailed: 'A implantação não aceitou esses valores; eles foram deixados para você corrigir.', + invalidNumber: 'Insira um número ou deixe em branco para usar o padrão.', + invalidCount: 'Insira um número inteiro de 1 a 50 ou deixe em branco para usar o padrão.', +} satisfies Record diff --git a/packages/client/ui-settings/locale/de.json b/packages/client/ui-settings/locale/de.json new file mode 100644 index 0000000000..c0a32190bd --- /dev/null +++ b/packages/client/ui-settings/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstellenrahmen festlegen", + "description": "Stellt das Grundformular und den Plug-in-Erweiterungsspeicherort der Einstellungsseite bereit." + } +} diff --git a/packages/client/ui-settings/locale/es.json b/packages/client/ui-settings/locale/es.json new file mode 100644 index 0000000000..f703fc3c6e --- /dev/null +++ b/packages/client/ui-settings/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Establecer marco de interfaz", + "description": "Proporciona el formulario básico y la ubicación de la extensión del complemento de la página de configuración." + } +} diff --git a/packages/client/ui-settings/locale/fr.json b/packages/client/ui-settings/locale/fr.json new file mode 100644 index 0000000000..4e500b24d0 --- /dev/null +++ b/packages/client/ui-settings/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Définir le cadre de l'interface", + "description": "Fournit le formulaire de base et l’emplacement de l’extension du plug-in de la page des paramètres." + } +} diff --git a/packages/client/ui-settings/locale/pt.json b/packages/client/ui-settings/locale/pt.json new file mode 100644 index 0000000000..4aebe79237 --- /dev/null +++ b/packages/client/ui-settings/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Definir quadro de interface", + "description": "Fornece o formulário básico e a localização da extensão do plug-in da página de configurações." + } +} diff --git a/packages/client/ui-settings/locale/ru.json b/packages/client/ui-settings/locale/ru.json new file mode 100644 index 0000000000..bd60ab3572 --- /dev/null +++ b/packages/client/ui-settings/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Установить фрейм интерфейса", + "description": "Предоставляет базовую форму и расположение расширения подключаемого модуля на странице настроек." + } +} diff --git a/packages/client/ui-shortcuts/locale/de.json b/packages/client/ui-shortcuts/locale/de.json new file mode 100644 index 0000000000..f6fef238f7 --- /dev/null +++ b/packages/client/ui-shortcuts/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zum Festlegen von Tastenkombinationen", + "description": "Tastaturkürzel anzeigen, eingeben und ändern." + } +} diff --git a/packages/client/ui-shortcuts/locale/es.json b/packages/client/ui-shortcuts/locale/es.json new file mode 100644 index 0000000000..e01ec944f6 --- /dev/null +++ b/packages/client/ui-shortcuts/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de configuración de teclas de acceso directo", + "description": "Ver, ingresar y modificar atajos de teclado." + } +} diff --git a/packages/client/ui-shortcuts/locale/fr.json b/packages/client/ui-shortcuts/locale/fr.json new file mode 100644 index 0000000000..bbaa18731d --- /dev/null +++ b/packages/client/ui-shortcuts/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de configuration des touches de raccourci", + "description": "Afficher, saisir et modifier les raccourcis clavier." + } +} diff --git a/packages/client/ui-shortcuts/locale/pt.json b/packages/client/ui-shortcuts/locale/pt.json new file mode 100644 index 0000000000..85767dbdff --- /dev/null +++ b/packages/client/ui-shortcuts/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de configuração de teclas de atalho", + "description": "Visualize, insira e modifique atalhos de teclado." + } +} diff --git a/packages/client/ui-shortcuts/locale/ru.json b/packages/client/ui-shortcuts/locale/ru.json new file mode 100644 index 0000000000..b9152041ee --- /dev/null +++ b/packages/client/ui-shortcuts/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс настройки сочетаний клавиш", + "description": "Просмотр, ввод и изменение сочетаний клавиш." + } +} diff --git a/packages/client/ui-shortcuts/src/client/index.ts b/packages/client/ui-shortcuts/src/client/index.ts index 9e0d508833..1de049aec6 100644 --- a/packages/client/ui-shortcuts/src/client/index.ts +++ b/packages/client/ui-shortcuts/src/client/index.ts @@ -8,7 +8,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-layout/client' import type {} from '@deepseek-ai/dsh-client-ui-settings/client' import { createShortcutsStore } from './store.ts' import { ShortcutReference, ShortcutsRow } from './Reference.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' import { fixedCommands } from './fixed.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { @@ -26,7 +26,7 @@ export const inject = ['shortcuts', 'locale', 'slots'] * @param ctx - plugin-owned client context. */ export function apply(ctx: Context): void { - ctx.effect(() => ctx.locale.register('shortcuts', { zh, en, ja }), 'shortcuts: dictionaries') + ctx.effect(() => ctx.locale.register('shortcuts', { zh, en, ja, es, fr, de, ru, pt }), 'shortcuts: dictionaries') const t = ctx.locale.bind('shortcuts') const handle = createShortcutsStore() const instance = handle.create() diff --git a/packages/client/ui-shortcuts/src/client/locales.ts b/packages/client/ui-shortcuts/src/client/locales.ts index e96e4457da..6261df423a 100644 --- a/packages/client/ui-shortcuts/src/client/locales.ts +++ b/packages/client/ui-shortcuts/src/client/locales.ts @@ -128,3 +128,183 @@ export const ja = { 'select': 'メニュー項目を選択', 'dismiss': 'メニューまたは最上位ダイアログを閉じる', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'edit-label': 'Editar acceso directo para {command}', + record: 'Presione un atajo', 'record-help': 'Suelta las teclas para guardar. La pestaña se mueve entre acciones; Esc se cancela.', + 'web-help': 'Combinaciones de navegador: Mod+/, Mod+Shift+, Mod+Shift+... Mod es Comando en Mac y Ctrl en otros lugares.', + 'unsupported-key': 'Esta clave no es compatible.', reserved: 'Esta combinación está reservada para acciones de edición de texto o del sistema.', + 'modifier-required': 'Incluya Comando, Ctrl o Alt en la combinación.', + 'too-many-keys': 'Mantenga presionadas como máximo dos teclas no modificadoras. Suelta las teclas para volver a intentarlo.', + 'macos-web-help': 'Utilice Comando+/, Comando+, Comando+barra invertida, Control+comillas invertidas, Comando+Opción+tecla o Comando+Mayús+tecla. También se admiten combinaciones con tres o cuatro modificadores distintos. Es posible que los accesos directos del navegador o del sistema no lleguen a la página.', + 'windows-web-help': 'Utilice Ctrl+/, Ctrl+, Ctrl+Alt+tecla o Ctrl+Shift+tecla. También se admiten combinaciones con tres o cuatro modificadores distintos. Es posible que los accesos directos del navegador o del sistema no lleguen a la página.', + 'unsupported-browser': 'Este navegador aún no admite esta combinación.', conflict: 'Ya utilizado por “{commands}”', + saved: 'Modificado', clear: 'Quitar', 'retry-save': 'Reintentar guardar', reset: 'Restaurar valor predeterminado', + 'reset-all': 'Restaurar todos los valores predeterminados', 'modified-count': '{count} personalizado', + 'reset-title': '¿Restaurar todos los atajos predeterminados?', + 'reset-description': 'Restaure los accesos directos predeterminados para esta plataforma. Se restaurarán todos los accesos directos modificados o eliminados. Otras plataformas no se ven afectadas.', + cancel: 'Cancelar', 'reset-saved': 'Accesos directos predeterminados restaurados', + 'close-confirmation': 'Cerrar confirmación', 'reset-failed': 'No se pudieron restaurar los valores predeterminados. Tus atajos no han cambiado. Vuelva a intentarlo.', + review: 'He revisado la última configuración.', + stale: 'La configuración de accesos directos o los comandos disponibles cambiaron. Revise los enlaces más recientes antes de guardar.', + 'write-failed': 'No se pudo guardar. Se conservan sus atajos anteriores y el borrador actual. Vuelva a intentarlo.', + 'not-ready': 'Los atajos no están listos. Por favor inténtalo de nuevo.', + read: 'No se pudo leer {location}. Verifique los permisos de acceso, luego {reload}.', + invalid: 'La configuración de acceso directo en {location} está dañada. Haga una copia de seguridad y repare esta configuración, luego {reload}.', + future: 'La configuración de acceso directo en {location} fue creada por una versión más reciente. Actualice el arnés e inténtelo de nuevo.', + 'web-document': 'la entrada de almacenamiento local de este sitio dsh.keybindings.v1', 'desktop-document': 'datos de usuario/bindings.json', + 'web-reload': 'recargar la pagina', 'desktop-reload': 'reiniciar el arnés', + 'using-defaults': 'Los enlaces predeterminados están activos.', 'using-accepted': 'Los últimos enlaces leídos correctamente permanecen activos.', + 'native-failed': 'No se pudo proteger la grabación de claves del escritorio. Salga de la grabación y vuelva a intentarlo.', + 'global-hint': 'Abierto desde cualquier lugar', 'clear-search': 'Borrar búsqueda', + title: 'Atajos de teclado', open: 'Abrir atajos de teclado', settings: 'Atajos de teclado', view: 'Editar atajos', + description: 'Ver y editar atajos disponibles y acciones de entrada', search: 'Buscar atajos', close: 'Cerrar atajos de teclado', + application: 'Solicitud', input: 'Entrada de mensaje', menus: 'Menús y cuadros de diálogo', approval: 'Área de aprobación', unbound: 'Sin atajos', + empty: 'No hay atajos coincidentes', + move: 'Mover selección de menú', select: 'Seleccionar elemento del menú', dismiss: 'Cerrar menú o cuadro de diálogo superior', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'edit-label': 'Modifier le raccourci pour {command}', + record: 'Appuyez sur un raccourci', 'record-help': "Relâchez les touches pour enregistrer. L'onglet se déplace entre les actions ; Esc annule.", + 'web-help': 'Combinaisons de navigateur : Mod+/, Mod+Shift+,, Mod+Shift+.. Mod est Command sur Mac et Ctrl ailleurs.', + 'unsupported-key': "Cette clé n'est pas prise en charge.", reserved: "Cette combinaison est réservée aux actions système ou d'édition de texte.", + 'modifier-required': 'Incluez Commande, Ctrl ou Alt dans la combinaison.', + 'too-many-keys': 'Tenez au plus deux clés non modificatrices. Relâchez les touches pour réessayer.', + 'macos-web-help': 'Utilisez Command+/, Command+,, Command+Backslash, Control+Backquote, Command+Option+key ou Command+Shift+key. Les combinaisons avec trois ou quatre modificateurs distincts sont également prises en charge. Les raccourcis du navigateur ou du système peuvent ne pas atteindre la page.', + 'windows-web-help': 'Utilisez Ctrl+/, Ctrl+,, Ctrl+Alt+touche ou Ctrl+Shift+touche. Les combinaisons avec trois ou quatre modificateurs distincts sont également prises en charge. Les raccourcis du navigateur ou du système peuvent ne pas atteindre la page.', + 'unsupported-browser': 'Ce navigateur ne prend pas encore en charge cette combinaison.', conflict: 'Déjà utilisé par « {commands} »', + saved: 'Modifié', clear: 'Supprimer', 'retry-save': "Réessayez d'enregistrer", reset: 'Restaurer les valeurs par défaut', + 'reset-all': 'Restaurer tous les paramètres par défaut', 'modified-count': '{count} personnalisé', + 'reset-title': 'Restaurer tous les raccourcis par défaut ?', + 'reset-description': 'Restaurez les raccourcis par défaut pour cette plateforme. Tous les raccourcis modifiés ou supprimés seront restaurés. Les autres plateformes ne sont pas affectées.', + cancel: 'Annuler', 'reset-saved': 'Raccourcis par défaut restaurés', + 'close-confirmation': 'Fermer la confirmation', 'reset-failed': 'Impossible de restaurer les valeurs par défaut. Vos raccourcis restent inchangés. Veuillez réessayer.', + review: "J'ai examiné la dernière configuration", + stale: 'La configuration des raccourcis ou les commandes disponibles ont été modifiées. Vérifiez les dernières liaisons avant de les enregistrer.', + 'write-failed': "Impossible d'enregistrer. Vos raccourcis précédents et votre brouillon actuel sont conservés. Veuillez réessayer.", + 'not-ready': 'Les raccourcis ne sont pas prêts. Veuillez réessayer.', + read: "Impossible de lire {location}. Vérifiez les autorisations d'accès, puis {reload}.", + invalid: 'La configuration des raccourcis dans {location} est endommagée. Sauvegardez et réparez cette configuration, puis {reload}.', + future: 'La configuration des raccourcis dans {location} a été créée par une version plus récente. Mettez à niveau le harnais et réessayez.', + 'web-document': "l'entrée localStorage de ce site dsh.keybindings.v1", 'desktop-document': 'userData/keybindings.json', + 'web-reload': 'recharger la page', 'desktop-reload': 'redémarrer le harnais', + 'using-defaults': 'Les liaisons par défaut sont actives.', 'using-accepted': 'Les dernières liaisons lues avec succès restent actives.', + 'native-failed': "Impossible de protéger l'enregistrement des clés du bureau. Quittez l'enregistrement et réessayez.", + 'global-hint': "Ouvert de n'importe où", 'clear-search': 'Effacer la recherche', + title: 'Raccourcis clavier', open: 'Ouvrir les raccourcis clavier', settings: 'Raccourcis clavier', view: 'Modifier les raccourcis', + description: 'Afficher et modifier les raccourcis et les actions de saisie disponibles', search: 'Raccourcis de recherche', close: 'Fermer les raccourcis clavier', + application: 'Demande', input: 'Saisie des messages', menus: 'Menus et boîtes de dialogue', approval: "Zone d'approbation", unbound: 'Pas de raccourci', + empty: 'Aucun raccourci correspondant', + move: 'Déplacer la sélection du menu', select: 'Sélectionner un élément de menu', dismiss: 'Fermer le menu ou la boîte de dialogue supérieure', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'edit-label': 'Verknüpfung für {command} bearbeiten', + record: 'Drücken Sie eine Verknüpfung', 'record-help': 'Lassen Sie die Tasten los, um zu speichern. Tab bewegt sich zwischen Aktionen; Esc bricht ab.', + 'web-help': 'Browserkombinationen: Mod+/, Mod+Umschalt+,, Mod+Umschalt+.. Mod ist Befehl auf dem Mac und Strg anderswo.', + 'unsupported-key': 'Dieser Schlüssel wird nicht unterstützt.', reserved: 'Diese Kombination ist für System- oder Textbearbeitungsaktionen reserviert.', + 'modifier-required': 'Fügen Sie Befehl, Strg oder Alt in die Kombination ein.', + 'too-many-keys': 'Halten Sie höchstens zwei nicht modifizierbare Tasten gedrückt. Lassen Sie die Tasten los, um es erneut zu versuchen.', + 'macos-web-help': 'Verwenden Sie Befehl+/, Befehl+,, Befehl+Backslash, Strg+Backquote, Befehl+Wahl+Taste oder Befehl+Umschalt+Taste. Kombinationen mit drei oder vier unterschiedlichen Modifikatoren werden ebenfalls unterstützt. Browser- oder Systemverknüpfungen erreichen die Seite möglicherweise nicht.', + 'windows-web-help': 'Verwenden Sie Strg+/, Strg+,, Strg+Alt+Taste oder Strg+Umschalt+Taste. Kombinationen mit drei oder vier unterschiedlichen Modifikatoren werden ebenfalls unterstützt. Browser- oder Systemverknüpfungen erreichen die Seite möglicherweise nicht.', + 'unsupported-browser': 'Dieser Browser unterstützt diese Kombination noch nicht.', conflict: 'Bereits verwendet von „{commands}“', + saved: 'Geändert', clear: 'Entfernen', 'retry-save': 'Speichern Sie erneut', reset: 'Standard wiederherstellen', + 'reset-all': 'Alle Standardeinstellungen wiederherstellen', 'modified-count': '{count} angepasst', + 'reset-title': 'Alle Standardverknüpfungen wiederherstellen?', + 'reset-description': 'Stellen Sie die Standardverknüpfungen für diese Plattform wieder her. Alle geänderten oder entfernten Verknüpfungen werden wiederhergestellt. Andere Plattformen sind davon nicht betroffen.', + cancel: 'Abbrechen', 'reset-saved': 'Standardverknüpfungen wiederhergestellt', + 'close-confirmation': 'Bestätigung schließen', 'reset-failed': 'Die Standardeinstellungen konnten nicht wiederhergestellt werden. Ihre Verknüpfungen bleiben unverändert. Bitte versuchen Sie es erneut.', + review: 'Ich habe die neueste Konfiguration überprüft', + stale: 'Verknüpfungskonfiguration oder verfügbare Befehle geändert. Überprüfen Sie vor dem Speichern die neuesten Bindungen.', + 'write-failed': 'Konnte nicht gespeichert werden. Ihre vorherigen Verknüpfungen und Ihr aktueller Entwurf bleiben erhalten. Bitte versuchen Sie es erneut.', + 'not-ready': 'Verknüpfungen sind nicht bereit. Bitte versuchen Sie es erneut.', + read: '{location} konnte nicht gelesen werden. Zugriffsberechtigungen prüfen, dann {reload}.', + invalid: 'Die Verknüpfungskonfiguration in {location} ist beschädigt. Sichern und reparieren Sie diese Konfiguration, dann {reload}.', + future: 'Die Verknüpfungskonfiguration in {location} wurde von einer neueren Version erstellt. Aktualisieren Sie Harness und versuchen Sie es erneut.', + 'web-document': 'Der localStorage-Eintrag dieser Site dsh.keybindings.v1', 'desktop-document': 'userData/keybindings.json', + 'web-reload': 'Laden Sie die Seite neu', 'desktop-reload': 'Starten Sie Harness neu', + 'using-defaults': 'Standardbindungen sind aktiv.', 'using-accepted': 'Die zuletzt erfolgreich gelesenen Bindungen bleiben aktiv.', + 'native-failed': 'Die Desktop-Schlüsselaufzeichnung konnte nicht geschützt werden. Beenden Sie die Aufnahme und versuchen Sie es erneut.', + 'global-hint': 'Von überall aus öffnen', 'clear-search': 'Suche löschen', + title: 'Tastaturkürzel', open: 'Öffnen Sie Tastaturkürzel', settings: 'Tastaturkürzel', view: 'Verknüpfungen bearbeiten', + description: 'Verfügbare Verknüpfungen und Eingabeaktionen anzeigen und bearbeiten', search: 'Suchverknüpfungen', close: 'Tastaturkürzel schließen', + application: 'Anwendung', input: 'Nachrichteneingabe', menus: 'Menüs und Dialoge', approval: 'Genehmigungsbereich', unbound: 'Keine Abkürzung', + empty: 'Keine passenden Verknüpfungen', + move: 'Menüauswahl verschieben', select: 'Menüpunkt auswählen', dismiss: 'Menü oder oberen Dialog schließen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'edit-label': 'Изменить ярлык для {command}', + record: 'Нажмите ярлык', 'record-help': 'Отпустите клавиши для сохранения. Tab перемещается между действиями; Esc отменяет.', + 'web-help': 'Комбинации браузера: Mod+/, Mod+Shift+, Mod+Shift+.. Mod — это команда на Mac, а в других местах — Ctrl.', + 'unsupported-key': 'Этот ключ не поддерживается.', reserved: 'Эта комбинация зарезервирована для системных действий или действий по редактированию текста.', + 'modifier-required': 'Включите в комбинацию Command, Ctrl или Alt.', + 'too-many-keys': 'Удерживайте не более двух немодифицирующих клавиш. Отпустите клавиши, чтобы повторить попытку.', + 'macos-web-help': 'Используйте Command+/, Command+,, Command+обратная косая черта, Control+обратная кавычка, Command+Option+клавиша или Command+Shift+клавиша. Также поддерживаются комбинации с тремя или четырьмя различными модификаторами. Ярлыки браузера или системы могут не доходить до страницы.', + 'windows-web-help': 'Используйте Ctrl+/, Ctrl+, Ctrl+Alt+клавиша или Ctrl+Shift+клавиша. Также поддерживаются комбинации с тремя или четырьмя различными модификаторами. Ярлыки браузера или системы могут не доходить до страницы.', + 'unsupported-browser': 'Этот браузер пока не поддерживает эту комбинацию.', conflict: 'Уже используется «{commands}»', + saved: 'Модифицированный', clear: 'Удалить', 'retry-save': 'Повторить попытку сохранения', reset: 'Восстановить настройки по умолчанию', + 'reset-all': 'Восстановить все настройки по умолчанию', 'modified-count': '{count} по индивидуальному заказу', + 'reset-title': 'Восстановить все ярлыки по умолчанию?', + 'reset-description': 'Восстановите ярлыки по умолчанию для этой платформы. Все измененные или удаленные ярлыки будут восстановлены. Другие платформы не затронуты.', + cancel: 'Отмена', 'reset-saved': 'Ярлыки по умолчанию восстановлены.', + 'close-confirmation': 'Закрыть подтверждение', 'reset-failed': 'Не удалось восстановить настройки по умолчанию. Ваши ярлыки не изменились. Пожалуйста, повторите попытку.', + review: 'Я рассмотрел последнюю конфигурацию', + stale: 'Конфигурация ярлыка или доступные команды изменены. Перед сохранением просмотрите последние привязки.', + 'write-failed': 'Не удалось сохранить. Ваши предыдущие ярлыки и текущий черновик сохраняются. Пожалуйста, повторите попытку.', + 'not-ready': 'Ярлыки не готовы. Пожалуйста, попробуйте еще раз.', + read: 'Не удалось прочитать {location}. Проверьте права доступа, затем {reload}.', + invalid: 'Конфигурация ярлыка в {location} повреждена. Создайте резервную копию и исправьте эту конфигурацию, затем {reload}.', + future: 'Конфигурация ярлыка в {location} была создана более новой версией. Обновите Harness и повторите попытку.', + 'web-document': 'запись localStorage этого сайта dsh.keybindings.v1', 'desktop-document': 'userData/keybindings.json', + 'web-reload': 'перезагрузить страницу', 'desktop-reload': 'перезапустить Harness', + 'using-defaults': 'Привязки по умолчанию активны.', 'using-accepted': 'Последние успешно прочитанные привязки остаются активными.', + 'native-failed': 'Не удалось защитить запись клавиш рабочего стола. Выйдите из записи и повторите попытку.', + 'global-hint': 'Открыто из любого места', 'clear-search': 'Очистить поиск', + title: 'Сочетания клавиш', open: 'Открытие сочетаний клавиш', settings: 'Сочетания клавиш', view: 'Редактировать ярлыки', + description: 'Просмотр и редактирование доступных ярлыков и действий ввода', search: 'Поиск ярлыков', close: 'Закрыть сочетания клавиш', + application: 'Приложение', input: 'Ввод сообщения', menus: 'Меню и диалоги', approval: 'Область утверждения', unbound: 'Нет ярлыка', + empty: 'Нет подходящих ярлыков', + move: 'Переместить выбор меню', select: 'Выберите пункт меню', dismiss: 'Закрыть меню или верхний диалог', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'edit-label': 'Editar atalho para {command}', + record: 'Pressione um atalho', 'record-help': 'Solte as teclas para salvar. A guia se move entre as ações; Esc cancela.', + 'web-help': 'Combinações de navegador: Mod+/, Mod+Shift+,, Mod+Shift+.. Mod é Command no Mac e Ctrl em outros lugares.', + 'unsupported-key': 'Esta chave não é suportada.', reserved: 'Esta combinação é reservada para ações de sistema ou edição de texto.', + 'modifier-required': 'Inclua Command, Ctrl ou Alt na combinação.', + 'too-many-keys': 'Segure no máximo duas teclas não modificadoras. Solte as teclas para tentar novamente.', + 'macos-web-help': 'Use Command+/, Command+,, Command+Barra invertida, Control+Backquote, Command+Option+key ou Command+Shift+key. Combinações com três ou quatro modificadores distintos também são suportadas. Os atalhos do navegador ou do sistema podem não chegar à página.', + 'windows-web-help': 'Use Ctrl+/, Ctrl+,, Ctrl+Alt+tecla ou Ctrl+Shift+tecla. Combinações com três ou quatro modificadores distintos também são suportadas. Os atalhos do navegador ou do sistema podem não chegar à página.', + 'unsupported-browser': 'Este navegador ainda não suporta esta combinação.', conflict: 'Já usado por “{commands}”', + saved: 'Modificado', clear: 'Remover', 'retry-save': 'Tentar salvar novamente', reset: 'Restaurar padrão', + 'reset-all': 'Restaurar todos os padrões', 'modified-count': '{count} personalizado', + 'reset-title': 'Restaurar todos os atalhos padrão?', + 'reset-description': 'Restaure os atalhos padrão desta plataforma. Todos os atalhos modificados ou removidos serão restaurados. Outras plataformas não são afetadas.', + cancel: 'Cancelar', 'reset-saved': 'Atalhos padrão restaurados', + 'close-confirmation': 'Fechar confirmação', 'reset-failed': 'Não foi possível restaurar os padrões. Seus atalhos permanecem inalterados. Por favor, tente novamente.', + review: 'Eu revisei a configuração mais recente', + stale: 'Configuração de atalhos ou comandos disponíveis alterados. Revise as ligações mais recentes antes de salvar.', + 'write-failed': 'Não foi possível salvar. Seus atalhos anteriores e o rascunho atual serão preservados. Por favor, tente novamente.', + 'not-ready': 'Os atalhos não estão prontos. Por favor, tente novamente.', + read: 'Não foi possível ler {location}. Verifique as permissões de acesso e depois {reload}.', + invalid: 'A configuração do atalho em {location} está danificada. Faça backup e repare esta configuração e então {reload}.', + future: 'A configuração do atalho em {location} foi criada por uma versão mais recente. Atualize o Harness e tente novamente.', + 'web-document': 'a entrada localStorage deste site dsh.keybindings.v1', 'desktop-document': 'userData/keybindings.json', + 'web-reload': 'recarregar a página', 'desktop-reload': 'reinicie o chicote', + 'using-defaults': 'As ligações padrão estão ativas.', 'using-accepted': 'As últimas ligações lidas com sucesso permanecem ativas.', + 'native-failed': 'Não foi possível proteger a gravação de teclas da área de trabalho. Saia da gravação e tente novamente.', + 'global-hint': 'Abra de qualquer lugar', 'clear-search': 'Limpar pesquisa', + title: 'Atalhos de teclado', open: 'Abra atalhos de teclado', settings: 'Atalhos de teclado', view: 'Editar atalhos', + description: 'Visualize e edite atalhos e ações de entrada disponíveis', search: 'Atalhos de pesquisa', close: 'Fechar atalhos de teclado', + application: 'Aplicação', input: 'Entrada de mensagem', menus: 'Menus e caixas de diálogo', approval: 'Área de aprovação', unbound: 'Sem atalho', + empty: 'Nenhum atalho correspondente', + move: 'Mover seleção de menu', select: 'Selecione o item do menu', dismiss: 'Fechar menu ou caixa de diálogo superior', +} satisfies Record diff --git a/packages/client/ui-sidebar-browser/locale/de.json b/packages/client/ui-sidebar-browser/locale/de.json new file mode 100644 index 0000000000..0e2de49b34 --- /dev/null +++ b/packages/client/ui-sidebar-browser/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Browser-Panel", + "description": "Öffnen und durchsuchen Sie Webseiten im rechten Bereich." + } +} diff --git a/packages/client/ui-sidebar-browser/locale/es.json b/packages/client/ui-sidebar-browser/locale/es.json new file mode 100644 index 0000000000..aab1b5b129 --- /dev/null +++ b/packages/client/ui-sidebar-browser/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "panel del navegador", + "description": "Abra y explore páginas web en el panel derecho." + } +} diff --git a/packages/client/ui-sidebar-browser/locale/fr.json b/packages/client/ui-sidebar-browser/locale/fr.json new file mode 100644 index 0000000000..1d11175a16 --- /dev/null +++ b/packages/client/ui-sidebar-browser/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "panneau de navigateur", + "description": "Ouvrez et parcourez les pages Web dans le panneau de droite." + } +} diff --git a/packages/client/ui-sidebar-browser/locale/pt.json b/packages/client/ui-sidebar-browser/locale/pt.json new file mode 100644 index 0000000000..88074aa070 --- /dev/null +++ b/packages/client/ui-sidebar-browser/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "painel do navegador", + "description": "Abra e navegue nas páginas da web no painel direito." + } +} diff --git a/packages/client/ui-sidebar-browser/locale/ru.json b/packages/client/ui-sidebar-browser/locale/ru.json new file mode 100644 index 0000000000..5ef851d55d --- /dev/null +++ b/packages/client/ui-sidebar-browser/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "панель браузера", + "description": "Открывайте и просматривайте веб-страницы на правой панели." + } +} diff --git a/packages/client/ui-sidebar-browser/src/client/index.ts b/packages/client/ui-sidebar-browser/src/client/index.ts index e3ac92a9db..29219212bc 100644 --- a/packages/client/ui-sidebar-browser/src/client/index.ts +++ b/packages/client/ui-sidebar-browser/src/client/index.ts @@ -16,7 +16,7 @@ import type { DesktopBrowserBridge } from '../types.ts' import { browserWorkspace } from './electron/workspace.ts' import type { BrowserPageFactory } from './browser/BrowserPage.ts' import { BROWSER_ID, browserDefinition } from './definition.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' import { createBrowserStore } from './browser/store.ts' export type { BrowserBodyProps } from './view/BrowserBody.tsx' @@ -70,7 +70,7 @@ export function apply(ctx: Context): void { dshDesktop?: { readonly protocolVersion: number; readonly browser?: DesktopBrowserBridge } }).dshDesktop const desktop = carrier?.protocolVersion === 1 ? carrier.browser : undefined - ctx.effect(() => ctx.locale.register(namespace, { zh, en, ja }), 'ui-sidebar-browser.copy') + ctx.effect(() => ctx.locale.register(namespace, { zh, en, ja, es, fr, de, ru, pt }), 'ui-sidebar-browser.copy') ctx.effect(() => ctx.sidebarRightTabs.register({ ...browserDefinition(t), keepMounted: desktop !== undefined }), 'ui-sidebar-browser.type') const installFrames = (scope: Context, factory: (sessionId: BrowserBodyProps['sessionId']) => BrowserPageFactory): void => { const controllers = new Map() diff --git a/packages/client/ui-sidebar-browser/src/client/locales.ts b/packages/client/ui-sidebar-browser/src/client/locales.ts index a5222248ae..fad29893e0 100644 --- a/packages/client/ui-sidebar-browser/src/client/locales.ts +++ b/packages/client/ui-sidebar-browser/src/client/locales.ts @@ -97,3 +97,153 @@ export const ja = { 'load.failed.detail': 'ページの読み込みに失敗しました({code}): {description}', 'address.unknown': 'ページが遷移しました。このキャリアは新しいURLを読み取れません。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'type.label': 'Navegador', + 'guide.title': 'Navegador', + 'guide.description': 'Navegar por páginas web', + 'shortcut.noSession': 'Primero abra una sesión', + 'address.placeholder': 'Ingrese una dirección HTTP(S)', + 'address.changed': 'URL cambiada', + back: 'Atrás', + forward: 'Adelante', + reload: 'recargar', + go: 'ir', + external: 'Abrir en el navegador del sistema', + 'sandbox.disable': 'Deshabilitar restricciones de zona de pruebas', + 'sandbox.enable': 'Restaurar restricciones de zona de pruebas', + 'sandbox.warning': 'Las restricciones de la zona de pruebas están deshabilitadas; la página puede navegar por la aplicación de nivel superior y usar descargas, cuadros de diálogo modales y bloqueos de entrada.', + start: 'Ingrese una dirección HTTP(S) para comenzar a navegar', + loading: 'Abriendo…', + 'restore.previous': 'Abierto previamente', + 'restore.action': 'Restaurar página', + 'error.empty': 'Introduzca una dirección.', + 'error.invalid': 'Esa dirección no es válida o es demasiado larga.', + 'error.protocol': 'Sólo se admiten las direcciones HTTP y HTTPS; utilice la vista previa del documento para archivos locales.', + 'error.credentials': 'Las direcciones no pueden contener un nombre de usuario o contraseña.', + 'error.application-origin': 'El navegador integrado no puede abrir la aplicación DSH.', + 'load.failed': 'La página no se pudo cargar; recargarlo o abrirlo en el navegador del sistema.', + 'load.failed.detail': 'Error al cargar la página ({code}): {description}', + 'address.unknown': 'La página navegó; este operador no puede leer su nueva URL.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'type.label': 'Navigateur', + 'guide.title': 'Navigateur', + 'guide.description': 'Parcourir les pages Web', + 'shortcut.noSession': "Ouvrir d'abord une session", + 'address.placeholder': 'Saisissez une adresse HTTP(S)', + 'address.changed': 'URL modifiée', + back: 'Retour', + forward: 'En avant', + reload: 'Recharger', + go: 'Allez', + external: 'Ouvrir dans le navigateur système', + 'sandbox.disable': 'Désactiver les restrictions du bac à sable', + 'sandbox.enable': 'Restaurer les restrictions du bac à sable', + 'sandbox.warning': "Les restrictions du bac à sable sont désactivées ; la page peut naviguer dans l'application de niveau supérieur et utiliser les téléchargements, les boîtes de dialogue modales et les verrous de saisie.", + start: 'Entrez une adresse HTTP(S) pour commencer la navigation', + loading: 'Ouverture…', + 'restore.previous': 'Déjà ouvert', + 'restore.action': 'Page de restauration', + 'error.empty': 'Entrez une adresse.', + 'error.invalid': "Cette adresse n'est pas valide ou est trop longue.", + 'error.protocol': "Seules les adresses HTTP et HTTPS sont prises en charge ; utilisez l'aperçu du document pour les fichiers locaux.", + 'error.credentials': "Les adresses ne peuvent pas contenir de nom d'utilisateur ou de mot de passe.", + 'error.application-origin': "Le navigateur intégré ne peut pas ouvrir l'application DSH elle-même.", + 'load.failed': "La page n'a pas pu se charger ; rechargez-le ou ouvrez-le dans le navigateur système.", + 'load.failed.detail': 'Échec du chargement de la page ({code}) : {description}', + 'address.unknown': 'La page parcourue ; cet opérateur ne peut pas lire sa nouvelle URL.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'type.label': 'Browser', + 'guide.title': 'Browser', + 'guide.description': 'Durchsuchen Sie Webseiten', + 'shortcut.noSession': 'Öffnen Sie zunächst eine Sitzung', + 'address.placeholder': 'Geben Sie eine HTTP(S)-Adresse ein', + 'address.changed': 'URL geändert', + back: 'Zurück', + forward: 'Vorwärts', + reload: 'Neu laden', + go: 'Geh', + external: 'Im Systembrowser öffnen', + 'sandbox.disable': 'Sandbox-Einschränkungen deaktivieren', + 'sandbox.enable': 'Sandbox-Einschränkungen wiederherstellen', + 'sandbox.warning': 'Sandbox-Einschränkungen sind deaktiviert; Die Seite kann durch die App der obersten Ebene navigieren und Downloads, modale Dialoge und Eingabesperren verwenden.', + start: 'Geben Sie eine HTTP(S)-Adresse ein, um mit dem Surfen zu beginnen', + loading: 'Eröffnung…', + 'restore.previous': 'Zuvor geöffnet', + 'restore.action': 'Seite wiederherstellen', + 'error.empty': 'Geben Sie eine Adresse ein.', + 'error.invalid': 'Diese Adresse ist ungültig oder zu lang.', + 'error.protocol': 'Es werden nur die Adressen HTTP und HTTPS unterstützt. Verwenden Sie die Dokumentvorschau für lokale Dateien.', + 'error.credentials': 'Adressen dürfen weder einen Benutzernamen noch ein Passwort enthalten.', + 'error.application-origin': 'Der eingebettete Browser kann die DSH-Anwendung selbst nicht öffnen.', + 'load.failed': 'Die Seite konnte nicht geladen werden; neu laden oder im Systembrowser öffnen.', + 'load.failed.detail': 'Laden der Seite fehlgeschlagen ({code}): {description}', + 'address.unknown': 'Die Seite wurde navigiert; Dieser Netzbetreiber kann seine neue URL nicht lesen.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'type.label': 'Браузер', + 'guide.title': 'Браузер', + 'guide.description': 'Просмотр веб-страниц', + 'shortcut.noSession': 'Сначала откройте сеанс', + 'address.placeholder': 'Введите адрес HTTP(S)', + 'address.changed': 'URL-адрес изменен', + back: 'Назад', + forward: 'Вперед', + reload: 'Перезагрузить', + go: 'Иди', + external: 'Открыть в системном браузере', + 'sandbox.disable': 'Отключить ограничения песочницы', + 'sandbox.enable': 'Восстановить ограничения песочницы', + 'sandbox.warning': 'Ограничения песочницы отключены; страница может перемещаться по приложению верхнего уровня и использовать загрузки, модальные диалоговые окна и блокировки ввода.', + start: 'Введите адрес HTTP(S), чтобы начать просмотр.', + loading: 'Открытие…', + 'restore.previous': 'Ранее открыто', + 'restore.action': 'Восстановить страницу', + 'error.empty': 'Введите адрес.', + 'error.invalid': 'Этот адрес недействителен или слишком длинный.', + 'error.protocol': 'Поддерживаются только адреса HTTP и HTTPS; используйте предварительный просмотр документа для локальных файлов.', + 'error.credentials': 'Адреса не могут содержать имя пользователя или пароль.', + 'error.application-origin': 'Встроенный браузер не может открыть само приложение DSH.', + 'load.failed': 'Страница не может загрузиться; перезагрузите или откройте его в системном браузере.', + 'load.failed.detail': 'Ошибка загрузки страницы ({code}): {description}', + 'address.unknown': 'Страница перемещалась; этот оператор связи не может прочитать свой новый URL-адрес.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'type.label': 'Navegador', + 'guide.title': 'Navegador', + 'guide.description': 'Navegar em páginas da web', + 'shortcut.noSession': 'Abra uma sessão primeiro', + 'address.placeholder': 'Insira um endereço HTTP(S)', + 'address.changed': 'URL alterado', + back: 'Voltar', + forward: 'Avançar', + reload: 'Recarregar', + go: 'Vá', + external: 'Abrir no navegador do sistema', + 'sandbox.disable': 'Desativar restrições de sandbox', + 'sandbox.enable': 'Restaurar restrições de sandbox', + 'sandbox.warning': 'As restrições do sandbox estão desativadas; a página pode navegar no aplicativo de nível superior e usar downloads, caixas de diálogo modais e bloqueios de entrada.', + start: 'Insira um endereço HTTP(S) para começar a navegar', + loading: 'Abrindo…', + 'restore.previous': 'Aberto anteriormente', + 'restore.action': 'Restaurar página', + 'error.empty': 'Insira um endereço.', + 'error.invalid': 'Esse endereço é inválido ou muito longo.', + 'error.protocol': 'Somente endereços HTTP e HTTPS são suportados; use a visualização do documento para arquivos locais.', + 'error.credentials': 'Os endereços não podem conter um nome de usuário ou senha.', + 'error.application-origin': 'O navegador incorporado não pode abrir o aplicativo DSH sozinho.', + 'load.failed': 'A página não pôde carregar; recarregue ou abra-o no navegador do sistema.', + 'load.failed.detail': 'Falha no carregamento da página ({code}): {description}', + 'address.unknown': 'A página navegada; esta operadora não consegue ler seu novo URL.', +} satisfies Record diff --git a/packages/client/ui-sidebar-browser/tests/apply.client.spec.ts b/packages/client/ui-sidebar-browser/tests/apply.client.spec.ts index 68f338a40d..c2a7078d60 100644 --- a/packages/client/ui-sidebar-browser/tests/apply.client.spec.ts +++ b/packages/client/ui-sidebar-browser/tests/apply.client.spec.ts @@ -11,7 +11,7 @@ import { BrowserTitle } from '../src/client/view/BrowserTitle.tsx' import type { BrowserInjected } from '../src/client/browser/BrowserController.ts' import { BROWSER_ID, BROWSER_KIND } from '../src/client/definition.tsx' import { apply, inject } from '../src/client/index.ts' -import { en, ja, zh } from '../src/client/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/locales.ts' import { createBrowserStore } from '../src/client/browser/store.ts' import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit' import type { DesktopBrowserBridge, DesktopBrowserLeaseId } from '../src/types.ts' @@ -125,7 +125,7 @@ describe('ui-sidebar-browser apply', () => { expect(definition?.title('sidebar://browser')).toBe('type.label') expect(definition?.guide?.map(entry => [entry.order, entry.title(), entry.description?.()])) .toEqual([[30, 'guide.title', 'guide.description']]) - expect(dictionaries.get('sidebarBrowser')).toEqual({ zh, en, ja }) + expect(dictionaries.get('sidebarBrowser')).toEqual({ zh, en, ja, es, fr, de, ru, pt }) expect(dictionaries.get('sidebarBrowser')).toMatchObject({ zh: { 'guide.description': '浏览网页' }, en: { 'guide.description': 'Browse web pages' }, diff --git a/packages/client/ui-sidebar-documentpreview/locale/de.json b/packages/client/ui-sidebar-documentpreview/locale/de.json new file mode 100644 index 0000000000..076c2d117b --- /dev/null +++ b/packages/client/ui-sidebar-documentpreview/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Dokumentvorschaufenster", + "description": "Vorschau von Dateien wie Dokumenten, Tabellen, Codes, Bildern und Webseiten im rechten Bereich." + } +} diff --git a/packages/client/ui-sidebar-documentpreview/locale/es.json b/packages/client/ui-sidebar-documentpreview/locale/es.json new file mode 100644 index 0000000000..67b4b65fb1 --- /dev/null +++ b/packages/client/ui-sidebar-documentpreview/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Panel de vista previa del documento", + "description": "Obtenga una vista previa de archivos como documentos, tablas, códigos, imágenes y páginas web en el panel derecho." + } +} diff --git a/packages/client/ui-sidebar-documentpreview/locale/fr.json b/packages/client/ui-sidebar-documentpreview/locale/fr.json new file mode 100644 index 0000000000..c06734b675 --- /dev/null +++ b/packages/client/ui-sidebar-documentpreview/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Panneau d'aperçu du document", + "description": "Prévisualisez les fichiers tels que les documents, les tableaux, les codes, les images et les pages Web sur le panneau de droite." + } +} diff --git a/packages/client/ui-sidebar-documentpreview/locale/pt.json b/packages/client/ui-sidebar-documentpreview/locale/pt.json new file mode 100644 index 0000000000..a2dc23158d --- /dev/null +++ b/packages/client/ui-sidebar-documentpreview/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Painel de visualização do documento", + "description": "Visualize arquivos como documentos, tabelas, códigos, imagens e páginas da web no painel direito." + } +} diff --git a/packages/client/ui-sidebar-documentpreview/locale/ru.json b/packages/client/ui-sidebar-documentpreview/locale/ru.json new file mode 100644 index 0000000000..42277eca20 --- /dev/null +++ b/packages/client/ui-sidebar-documentpreview/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Панель предварительного просмотра документа", + "description": "Предварительный просмотр файлов, таких как документы, таблицы, коды, изображения и веб-страницы, на правой панели." + } +} diff --git a/packages/client/ui-sidebar-documentpreview/src/client/code/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/code/index.ts index 3008802e74..eeceda36c4 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/code/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/code/index.ts @@ -3,14 +3,14 @@ import type { Context } from '@deepseek-ai/cordis' import { CODE_HIGHLIGHT_EXTENSIONS } from '@deepseek-ai/dsh-client-ui-primitives' import type {} from '../index.ts' import { CodeBody } from './CodeBody.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' const ID = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/code' const NS = 'sidebarCodePreview' /** @param ctx - owning plugin context. Register localized metadata and the matching keyed document body. */ export function apply(ctx: Context): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja })) + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt })) const t = ctx.locale.bind(NS) ctx.effect(() => ctx.documentPreviews.register({ id: ID, diff --git a/packages/client/ui-sidebar-documentpreview/src/client/code/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/code/locales.ts index 0173b40bff..04f2e115ed 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/code/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/code/locales.ts @@ -28,3 +28,38 @@ export const ja = { 'copy': 'コピー', 'copied': 'コピーしました', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'Código', + copy: 'Copiar', + copied: 'Copiado', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: 'Coder', + copy: 'Copier', + copied: 'Copié', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'Code', + copy: 'Kopieren', + copied: 'Kopiert', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'Код', + copy: 'Копировать', + copied: 'Скопировано', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'Código', + copy: 'Copiar', + copied: 'Copiado', +} satisfies Record diff --git a/packages/client/ui-sidebar-documentpreview/src/client/excel/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/excel/index.ts index 4a2771723f..28a2336475 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/excel/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/excel/index.ts @@ -3,7 +3,7 @@ import type { Context } from '@deepseek-ai/cordis' import type {} from '../index.ts' import type { Config } from '../../config.ts' import { LazyExcelBody } from './LazyExcelBody.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' /** * Register the browser Excel viewer and its lifecycle-owned slot. @@ -12,7 +12,7 @@ import { en, ja, zh } from './locales.ts' */ export function apply(ctx: Context, limits: Config['excel']): void { const id = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/excel' - ctx.effect(() => ctx.locale.register('sidebarExcel', { zh, en, ja })) + ctx.effect(() => ctx.locale.register('sidebarExcel', { zh, en, ja, es, fr, de, ru, pt })) const t = ctx.locale.bind('sidebarExcel') ctx.effect(() => ctx.documentPreviews.register({ id, extensions: ['xlsx', 'xls', 'csv', 'tsv'], binaryExtensions: ['xlsx', 'xls'], priority: 'builtin', diff --git a/packages/client/ui-sidebar-documentpreview/src/client/excel/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/excel/locales.ts index 2a448c0660..c45ad60880 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/excel/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/excel/locales.ts @@ -50,3 +50,63 @@ export const ja = { 'featureSeparator': '、', 'retry': '再試行', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'Hoja de cálculo', language: 'en', loading: 'Representando documento...', + invalid: 'Esta hoja de cálculo no se pudo abrir. Comprueba su formato, contenidos o protección con contraseña.', + tooLarge: 'Este libro excede el límite de tamaño de vista previa.', timeout: 'Se agotó el tiempo de apertura de este libro. Pruebe con un archivo más pequeño.', + encoding: 'Esta codificación de texto no se pudo leer. Guarde el archivo como UTF-8 o UTF-16 con una lista de materiales y vuelva a intentarlo.', + formulaWarning: 'Este libro contiene fórmulas. Los resultados mostrados pueden faltar o ser inexactos.', + unsupportedNotice: 'Esta vista previa no es compatible con {features} en este libro. Ábralo en una aplicación del sistema para disfrutar de la experiencia completa.', + charts: 'gráficos', images: 'imágenes', shapes: 'formas', conditionalFormatting: 'formato condicional', featureSeparator: ', ', + retry: 'Reintentar', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: 'Feuille de calcul', language: 'en', loading: 'Rendu du document...', + invalid: "Cette feuille de calcul n'a pas pu être ouverte. Vérifiez son format, son contenu ou sa protection par mot de passe.", + tooLarge: "Ce classeur dépasse la limite de taille d'aperçu.", timeout: "L'ouverture de ce classeur a expiré. Essayez un fichier plus petit.", + encoding: "Cet encodage de texte n'a pas pu être lu. Enregistrez le fichier au format UTF-8 ou UTF-16 avec une nomenclature et réessayez.", + formulaWarning: 'Ce classeur contient des formules. Les résultats affichés peuvent être manquants ou inexacts.', + unsupportedNotice: 'Cet aperçu ne prend pas en charge {features} dans ce classeur. Ouvrez-le dans une application système pour une expérience complète.', + charts: 'graphiques', images: 'photos', shapes: 'formes', conditionalFormatting: 'mise en forme conditionnelle', featureSeparator: ', ', + retry: 'Réessayer', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'Tabellenkalkulation', language: 'en', loading: 'Dokument wird gerendert...', + invalid: 'Diese Tabelle konnte nicht geöffnet werden. Überprüfen Sie Format, Inhalt oder Passwortschutz.', + tooLarge: 'Diese Arbeitsmappe überschreitet die Größenbeschränkung für die Vorschau.', timeout: 'Beim Öffnen dieser Arbeitsmappe ist eine Zeitüberschreitung aufgetreten. Versuchen Sie es mit einer kleineren Datei.', + encoding: 'Diese Textkodierung konnte nicht gelesen werden. Speichern Sie die Datei als UTF-8 oder UTF-16 mit einer Stückliste und versuchen Sie es erneut.', + formulaWarning: 'Diese Arbeitsmappe enthält Formeln. Die angezeigten Ergebnisse fehlen möglicherweise oder sind ungenau.', + unsupportedNotice: 'Diese Vorschau unterstützt {features} in dieser Arbeitsmappe nicht. Öffnen Sie es in einer Systemanwendung, um das volle Erlebnis zu genießen.', + charts: 'Diagramme', images: 'Bilder', shapes: 'Formen', conditionalFormatting: 'bedingte Formatierung', featureSeparator: ', ', + retry: 'Erneut versuchen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'электронная таблица', language: 'en', loading: 'Оформление документа...', + invalid: 'Эту таблицу невозможно открыть. Проверьте его формат, содержимое или защиту паролем.', + tooLarge: 'Размер этой книги превышает ограничение на размер предварительного просмотра.', timeout: 'Время открытия этой книги истекло. Попробуйте файл меньшего размера.', + encoding: 'Эту текстовую кодировку невозможно прочитать. Сохраните файл как UTF-8 или UTF-16 со спецификацией и повторите попытку.', + formulaWarning: 'В этой рабочей тетради есть формулы. Отображаемые результаты могут отсутствовать или быть неточными.', + unsupportedNotice: 'Эта предварительная версия не поддерживает {features} в этой книге. Откройте его в системном приложении, чтобы получить полную информацию.', + charts: 'графики', images: 'изображения', shapes: 'формы', conditionalFormatting: 'условное форматирование', featureSeparator: ', ', + retry: 'Повторить', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'Planilha', language: 'en', loading: 'Renderizando documento...', + invalid: 'Não foi possível abrir esta planilha. Verifique seu formato, conteúdo ou proteção por senha.', + tooLarge: 'Esta pasta de trabalho excede o limite de tamanho de visualização.', timeout: 'A abertura desta pasta de trabalho expirou. Experimente um arquivo menor.', + encoding: 'Esta codificação de texto não pôde ser lida. Salve o arquivo como UTF-8 ou UTF-16 com uma BOM e tente novamente.', + formulaWarning: 'Esta pasta de trabalho contém fórmulas. Os resultados exibidos podem estar ausentes ou imprecisos.', + unsupportedNotice: 'Esta visualização não oferece suporte a {features} nesta pasta de trabalho. Abra-o em um aplicativo do sistema para ter uma experiência completa.', + charts: 'gráficos', images: 'imagens', shapes: 'formas', conditionalFormatting: 'formatação condicional', featureSeparator: ', ', + retry: 'Tentar novamente', +} satisfies Record diff --git a/packages/client/ui-sidebar-documentpreview/src/client/html/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/html/index.ts index 056efb2de9..a731edabdf 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/html/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/html/index.ts @@ -6,7 +6,7 @@ import { hostFileOf } from '../rpc.ts' import { HtmlBody } from './HtmlBody.tsx' import type { HtmlBodyInjected } from './HtmlBody.tsx' import type {} from '@deepseek-ai/dsh-client-ui-settings/client' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' /** HTML implementation identity, shared by metadata and the keyed slot. */ export const HTML_BODY_ID = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/html' @@ -26,7 +26,7 @@ export function htmlBodyDefinition(title: () => string): DocumentPreviewDefiniti */ export function apply(ctx: Context): void { const t = ctx.locale.bind('documentHtml') - ctx.effect(() => ctx.locale.register('documentHtml', { zh, en, ja })) + ctx.effect(() => ctx.locale.register('documentHtml', { zh, en, ja, es, fr, de, ru, pt })) ctx.effect(() => ctx.documentPreviews.register(htmlBodyDefinition(() => t('title')))) ctx.effect(() => ctx.slots.inject('sidebar.right.tab.document', () => ctx.slots.register( { diff --git a/packages/client/ui-sidebar-documentpreview/src/client/html/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/html/locales.ts index 1617bd42d7..1751249d3b 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/html/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/html/locales.ts @@ -31,3 +31,43 @@ export const ja = { 'loading': 'ドキュメントをレンダリング中...', 'failed': 'このHTMLドキュメントをプレビューできませんでした。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'HTML', + frame: 'Vista previa del documento HTML', + loading: 'Representando documento...', + failed: 'No se pudo obtener una vista previa de este documento HTML.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: 'HTML', + frame: 'Aperçu du document HTML', + loading: 'Rendu du document...', + failed: "Ce document HTML n'a pas pu être prévisualisé.", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'HTML', + frame: 'Vorschau des HTML-Dokuments', + loading: 'Dokument wird gerendert...', + failed: 'Dieses HTML-Dokument konnte nicht in der Vorschau angezeigt werden.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'HTML', + frame: 'Предварительный просмотр HTML-документа', + loading: 'Оформление документа...', + failed: 'Этот HTML-документ не удалось просмотреть.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'HTML', + frame: 'Pré-visualização do documento HTML', + loading: 'Renderizando documento...', + failed: 'Este documento HTML não pôde ser visualizado.', +} satisfies Record diff --git a/packages/client/ui-sidebar-documentpreview/src/client/image/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/image/index.ts index 014598add0..a123cff8ae 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/image/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/image/index.ts @@ -8,7 +8,7 @@ import type { DocumentPreviewDefinition } from '../document/registry.ts' import { retainDocumentTabs } from '../document/tab-lifetime.ts' import { createZoomStore, type ZoomStore } from '../zoom/store.ts' import { ImageBody, type ImageBodyInjected } from './ImageBody.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' /** Image implementation identity, shared by metadata and the keyed slot. */ export const IMAGE_BODY_ID = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/image' @@ -42,7 +42,7 @@ export function imageBodyDefinition(title: () => string): DocumentPreviewDefinit */ export function apply(ctx: Context): void { const t = ctx.locale.bind('sidebarImage') - ctx.effect(() => ctx.locale.register('sidebarImage', { zh, en, ja }), 'document-image: dictionaries') + ctx.effect(() => ctx.locale.register('sidebarImage', { zh, en, ja, es, fr, de, ru, pt }), 'document-image: dictionaries') ctx.effect(() => ctx.documentPreviews.register(imageBodyDefinition(() => t('title'))), 'document-image: metadata') const store = createZoomStore() const retainTab = retainDocumentTabs(ctx) diff --git a/packages/client/ui-sidebar-documentpreview/src/client/image/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/image/locales.ts index 852635dcca..605e6d9e88 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/image/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/image/locales.ts @@ -1,4 +1,4 @@ -import { zoomEn, zoomJa, zoomZh } from '../zoom/locales.ts' +import { zoomEn, zoomJa, zoomEs, zoomFr, zoomDe, zoomRu, zoomPt, zoomZh } from '../zoom/locales.ts' /** Locale-owned image renderer labels and status text. */ export const zh = { @@ -39,3 +39,53 @@ export const ja = { 'failed': 'この画像を表示できませんでした。', 'unsupported': '画像プレビューには完全なファイル内容が必要です。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + ...zoomEs, + title: 'Imagen', + preview: 'Vista previa de la imagen: {name}', + loading: 'Representando documento...', + failed: 'Esta imagen no se pudo mostrar.', + unsupported: 'La vista previa de la imagen requiere el contenido completo del archivo.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + ...zoomFr, + title: 'Images', + preview: "Aperçu de l'image : {name}", + loading: 'Rendu du document...', + failed: "Cette image n'a pas pu être affichée.", + unsupported: "L'aperçu de l'image nécessite le contenu complet du fichier.", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + ...zoomDe, + title: 'Bild', + preview: 'Bildvorschau: {name}', + loading: 'Dokument wird gerendert...', + failed: 'Dieses Bild konnte nicht angezeigt werden.', + unsupported: 'Für die Bildvorschau ist der vollständige Dateiinhalt erforderlich.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + ...zoomRu, + title: 'Изображение', + preview: 'Предварительный просмотр изображения: {name}', + loading: 'Оформление документа...', + failed: 'Это изображение не может быть отображено.', + unsupported: 'Для предварительного просмотра изображения требуется полное содержимое файла.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + ...zoomPt, + title: 'Imagem', + preview: 'Visualização da imagem: {name}', + loading: 'Renderizando documento...', + failed: 'Esta imagem não pôde ser exibida.', + unsupported: 'A visualização da imagem requer o conteúdo completo do arquivo.', +} satisfies Record diff --git a/packages/client/ui-sidebar-documentpreview/src/client/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/index.ts index 1da0150e46..670ba33d6e 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/index.ts @@ -27,7 +27,7 @@ import { TEXTPREVIEW_ID, textDefinition } from './definition.ts' import { textFace } from './face.ts' import { createReadPage } from './rpc.ts' import { createTextStore } from './store.ts' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' import { DocumentPreviewRegistry } from './document/registry.ts' import { documentTabInfoFactory } from './document/contract.ts' import { apply as registerText } from './text/index.ts' @@ -91,7 +91,7 @@ export function apply(ctx: ClientContext): void { const disposePreviews = ctx.reflect.provide('documentPreviews', previews) ctx.effect(() => disposePreviews) ctx.effect(() => ctx.sidebarRightTabs.register(textDefinition()), 'ui-sidebar-documentpreview: text type') - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-sidebar-documentpreview: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-sidebar-documentpreview: dictionaries') const store = createTextStore() const face = textFace( diff --git a/packages/client/ui-sidebar-documentpreview/src/client/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/locales.ts index 400118427e..fa99836610 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/locales.ts @@ -86,3 +86,133 @@ export const ja = { 'error.unavailable': '読み込みに失敗しました: {message}', 'retry': '再試行', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + loading: 'Representando documento...', + loadMore: 'Cargar más', + changed: 'El archivo ha cambiado, mostrando el contenido anterior.', + reloadNow: 'recargar', + reload: 'Leer el archivo nuevamente', + autoRefresh: 'Actualización automática', + 'autoRefresh.enable': 'Habilitar actualización automática', + 'autoRefresh.disable': 'Deshabilitar la actualización automática', + 'wrap.enable': 'Activar ajuste de línea', + 'wrap.disable': 'Desactivar ajuste de línea', + 'wrap.aria': 'Ajuste de línea', + openWith: 'abrir con', + 'viewer.text': 'Texto sin formato', + resourceUnavailable: 'El servicio de recursos de archivos no está disponible.', + rendererUnavailable: 'La vista previa de {name} no está disponible.', + unsupportedFile: 'La vista previa aún no está disponible para este tipo de archivo.', + 'error.notFound': 'Archivo no encontrado. Es posible que se haya movido o eliminado.', + 'error.tooLarge': 'Esta página excede el límite {limit} y no se puede leer.', + 'error.notText': 'La vista previa aún no está disponible para este tipo de archivo.', + 'error.notRegularFile': 'No es un archivo normal, no hay nada que mostrar.', + 'error.unavailable': 'Error de lectura: {message}', + retry: 'Reintentar', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + loading: 'Rendu du document...', + loadMore: 'Charger plus', + changed: 'Le fichier a changé, affichant le contenu précédent.', + reloadNow: 'Recharger', + reload: 'Relisez le fichier', + autoRefresh: 'Actualisation automatique', + 'autoRefresh.enable': "Activer l'actualisation automatique", + 'autoRefresh.disable': "Désactiver l'actualisation automatique", + 'wrap.enable': 'Activer le retour à la ligne', + 'wrap.disable': 'Désactiver le retour à la ligne', + 'wrap.aria': 'Retour à la ligne', + openWith: 'Ouvrir avec', + 'viewer.text': 'Texte brut', + resourceUnavailable: "Le service de ressources de fichiers n'est pas disponible.", + rendererUnavailable: "L'aperçu {name} n'est pas disponible.", + unsupportedFile: "L'aperçu n'est pas encore disponible pour ce type de fichier.", + 'error.notFound': 'Fichier introuvable. Il a peut-être été déplacé ou supprimé.', + 'error.tooLarge': 'Cette page dépasse la limite {limit} et ne peut pas être lue.', + 'error.notText': "L'aperçu n'est pas encore disponible pour ce type de fichier.", + 'error.notRegularFile': "Ce n'est pas un fichier normal, rien à afficher.", + 'error.unavailable': 'Échec de lecture : {message}', + retry: 'Réessayer', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + loading: 'Dokument wird gerendert...', + loadMore: 'Mehr laden', + changed: 'Die Datei wurde geändert und zeigt den vorherigen Inhalt an.', + reloadNow: 'Neu laden', + reload: 'Lesen Sie die Datei noch einmal', + autoRefresh: 'Automatische Aktualisierung', + 'autoRefresh.enable': 'Aktivieren Sie die automatische Aktualisierung', + 'autoRefresh.disable': 'Deaktivieren Sie die automatische Aktualisierung', + 'wrap.enable': 'Aktivieren Sie den Zeilenumbruch', + 'wrap.disable': 'Schalten Sie den Zeilenumbruch aus', + 'wrap.aria': 'Zeilenumbruch', + openWith: 'Öffnen mit', + 'viewer.text': 'Klartext', + resourceUnavailable: 'Der Dateiressourcendienst ist nicht verfügbar.', + rendererUnavailable: 'Die {name}-Vorschau ist nicht verfügbar.', + unsupportedFile: 'Für diesen Dateityp ist noch keine Vorschau verfügbar.', + 'error.notFound': 'Datei nicht gefunden. Möglicherweise wurde es verschoben oder gelöscht.', + 'error.tooLarge': 'Diese Seite überschreitet das {limit}-Limit und kann nicht gelesen werden.', + 'error.notText': 'Für diesen Dateityp ist noch keine Vorschau verfügbar.', + 'error.notRegularFile': 'Keine normale Datei, nichts zum Anzeigen.', + 'error.unavailable': 'Lesen fehlgeschlagen: {message}', + retry: 'Erneut versuchen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + loading: 'Оформление документа...', + loadMore: 'Загрузить больше', + changed: 'Файл изменился, показывая предыдущее содержимое.', + reloadNow: 'Перезагрузить', + reload: 'Прочтите файл еще раз', + autoRefresh: 'Автоматическое обновление', + 'autoRefresh.enable': 'Включить автоматическое обновление', + 'autoRefresh.disable': 'Отключить автоматическое обновление', + 'wrap.enable': 'Включить перенос строк', + 'wrap.disable': 'Отключить перенос строк', + 'wrap.aria': 'Перенос строки', + openWith: 'Открыть с помощью', + 'viewer.text': 'Обычный текст', + resourceUnavailable: 'Служба файловых ресурсов недоступна.', + rendererUnavailable: 'Предварительный просмотр {name} недоступен.', + unsupportedFile: 'Предварительный просмотр для этого типа файлов пока недоступен.', + 'error.notFound': 'Файл не найден. Возможно, оно было перемещено или удалено.', + 'error.tooLarge': 'Эта страница превышает лимит {limit} и не может быть прочитана.', + 'error.notText': 'Предварительный просмотр для этого типа файлов пока недоступен.', + 'error.notRegularFile': 'Не обычный файл, отображать нечего.', + 'error.unavailable': 'Не удалось прочитать: {message}.', + retry: 'Повторить', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + loading: 'Renderizando documento...', + loadMore: 'Carregar mais', + changed: 'O arquivo foi alterado, mostrando o conteúdo anterior.', + reloadNow: 'Recarregar', + reload: 'Leia o arquivo novamente', + autoRefresh: 'Atualização automática', + 'autoRefresh.enable': 'Ativar atualização automática', + 'autoRefresh.disable': 'Desativar atualização automática', + 'wrap.enable': 'Ativar quebra de linha', + 'wrap.disable': 'Desativar quebra de linha', + 'wrap.aria': 'Quebra de linha', + openWith: 'Abrir com', + 'viewer.text': 'Texto simples', + resourceUnavailable: 'O serviço de recurso de arquivo não está disponível.', + rendererUnavailable: 'A visualização {name} não está disponível.', + unsupportedFile: 'A visualização ainda não está disponível para este tipo de arquivo.', + 'error.notFound': 'Arquivo não encontrado. Pode ter sido movido ou excluído.', + 'error.tooLarge': 'Esta página excede o limite de {limit} e não pode ser lida.', + 'error.notText': 'A visualização ainda não está disponível para este tipo de arquivo.', + 'error.notRegularFile': 'Não é um arquivo normal, nada para exibir.', + 'error.unavailable': 'Falha na leitura: {message}', + retry: 'Tentar novamente', +} satisfies Record diff --git a/packages/client/ui-sidebar-documentpreview/src/client/markdown/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/markdown/index.ts index a3d68990c0..2412dda05d 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/markdown/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/markdown/index.ts @@ -3,7 +3,7 @@ import type { Context } from '@deepseek-ai/cordis' import type {} from '../index.ts' import type { DocumentPreviewDefinition } from '../document/registry.ts' import { MarkdownBody } from './MarkdownBody.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' /** Implementation identity shared by metadata and the document slot. */ export const MARKDOWN_BODY_ID = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/markdown' @@ -23,7 +23,7 @@ export function markdownDefinition(title: () => string): DocumentPreviewDefiniti */ export function apply(ctx: Context): void { const t = ctx.locale.bind('documentMarkdown') - ctx.effect(() => ctx.locale.register('documentMarkdown', { zh, en, ja }), 'document-markdown: dictionaries') + ctx.effect(() => ctx.locale.register('documentMarkdown', { zh, en, ja, es, fr, de, ru, pt }), 'document-markdown: dictionaries') ctx.effect(() => ctx.documentPreviews.register(markdownDefinition(() => t('viewer.label'))), 'document-markdown: metadata') ctx.effect(() => ctx.slots.inject('sidebar.right.tab.document', () => ctx.slots.register( { name: 'sidebar.right.tab.document', key: MARKDOWN_BODY_ID, locale: 'documentMarkdown' }, MarkdownBody, diff --git a/packages/client/ui-sidebar-documentpreview/src/client/markdown/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/markdown/locales.ts index 41554ace8b..4405800591 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/markdown/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/markdown/locales.ts @@ -31,3 +31,43 @@ export const ja = { 'code.copied': 'コピーしました', 'footnotes': '脚注', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'viewer.label': 'Markdown', + 'code.copy': 'Copiar', + 'code.copied': 'Copiado', + 'footnotes': 'Notas a pie de página', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'viewer.label': 'Markdown', + 'code.copy': 'Copier', + 'code.copied': 'Copié', + 'footnotes': 'Notes de bas de page', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'viewer.label': 'Markdown', + 'code.copy': 'Kopieren', + 'code.copied': 'Kopiert', + 'footnotes': 'Fußnoten', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'viewer.label': 'Markdown', + 'code.copy': 'Копировать', + 'code.copied': 'Скопировано', + 'footnotes': 'Сноски', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'viewer.label': 'Markdown', + 'code.copy': 'Copiar', + 'code.copied': 'Copiado', + 'footnotes': 'Notas de rodapé', +} satisfies Record diff --git a/packages/client/ui-sidebar-documentpreview/src/client/office/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/office/index.ts index 350585f7ed..832787aaf2 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/office/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/office/index.ts @@ -7,7 +7,7 @@ import type {} from '@deepseek-ai/dsh-client-locale/client' import type {} from '@deepseek-ai/dsh-api-workspace-files/remote' import { failureLine } from '../failure-line.ts' import { documentTabInfoFactory } from '../document/contract.ts' -import { en, ja, zh, type OfficePreviewKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type OfficePreviewKey } from './locales.ts' import { OfficePreviewCache, type ReadOfficeBytes, type ReadOfficeDocument } from './cache.ts' import { pdfBodyRegistration } from '../pdf/index.ts' import { LazyPdfBody } from '../pdf/LazyPdfBody.tsx' @@ -31,7 +31,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { export function apply(ctx: Context, config: Config['office']): void { const id = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/office' const extensions = ['doc', 'docx', 'ppt', 'pptx'] - ctx.effect(() => ctx.locale.register('sidebarOffice', { zh, en, ja })) + ctx.effect(() => ctx.locale.register('sidebarOffice', { zh, en, ja, es, fr, de, ru, pt })) const t = ctx.locale.bind('sidebarOffice') const unavailable: ReadOfficeDocument = (_file, signal) => { signal.throwIfAborted() diff --git a/packages/client/ui-sidebar-documentpreview/src/client/office/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/office/locales.ts index fbab4f4913..5c224c81e4 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/office/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/office/locales.ts @@ -57,3 +57,98 @@ export const ja = { 'busy': 'Office プレビューは混雑しています。しばらくしてから再試行してください。', 'changed': '読み込み中にファイルが変更されました。プレビューを開き直してください。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + title: 'documento de oficina', + loading: 'Representando documento...', + retry: 'Reintentar', + viewMissingFonts: 'Fuentes faltantes: {count}. Haga clic para ver.', + missingFontsTitle: 'Fuentes faltantes', + missingFontsDescription: 'Estas fuentes no están disponibles para esta vista previa. El texto y el diseño pueden diferir del documento original.', + missingFontsCount: 'Fuentes: {count}', + closeDetails: 'Cerrar detalles de fuente', + unavailable: 'Las vistas previas de Office no están disponibles. Habilite el servicio de vista previa de documentos en la computadora que ejecuta DeepSeek Harness.', + invalid: 'No se puede obtener una vista previa de este archivo de Office. Puede que esté dañado, protegido con contraseña o que tenga la extensión incorrecta.', + tooLarge: 'El archivo de Office o el PDF convertido supera el límite de tamaño de vista previa. Reduzca el tamaño del archivo o ajuste la configuración de vista previa.', + failed: 'La conversión de Office no produjo un PDF utilizable. Verifique el archivo e inténtelo nuevamente.', + timeout: 'Se agotó el tiempo de espera para la conversión de Office. Intentar otra vez.', + busy: 'La vista previa de Office está ocupada. Vuelve a intentarlo en breve.', + changed: 'El archivo cambió mientras se leía. Vuelva a abrir la vista previa.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + title: 'Document de bureau', + loading: 'Rendu du document...', + retry: 'Réessayer', + viewMissingFonts: 'Polices manquantes : {count}. Cliquez pour voir.', + missingFontsTitle: 'Polices manquantes', + missingFontsDescription: 'Ces polices ne sont pas disponibles pour cet aperçu. Le texte et la mise en page peuvent différer du document original.', + missingFontsCount: 'Polices : {count}', + closeDetails: 'Fermer les détails de la police', + unavailable: "Les aperçus Office ne sont pas disponibles. Activez le service de prévisualisation des documents sur l'ordinateur exécutant DeepSeek Harness.", + invalid: 'Ce fichier Office ne peut pas être prévisualisé. Il peut être endommagé, protégé par mot de passe ou avoir une extension incorrecte.', + tooLarge: "Le fichier Office ou le PDF converti dépasse la limite de taille d'aperçu. Réduisez la taille du fichier ou ajustez la configuration de l’aperçu.", + failed: "La conversion Office n'a pas produit de PDF utilisable. Vérifiez le fichier et réessayez.", + timeout: 'La conversion du bureau a expiré. Essayer à nouveau.', + busy: 'L’aperçu Office est occupé. Réessayez sous peu.', + changed: "Le fichier a changé lors de sa lecture. Rouvrez l'aperçu.", +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + title: 'Office-Dokument', + loading: 'Dokument wird gerendert...', + retry: 'Erneut versuchen', + viewMissingFonts: 'Fehlende Schriftarten: {count}. Klicken Sie zum Anzeigen.', + missingFontsTitle: 'Fehlende Schriftarten', + missingFontsDescription: 'Diese Schriftarten sind für diese Vorschau nicht verfügbar. Text und Layout können vom Originaldokument abweichen.', + missingFontsCount: 'Schriftarten: {count}', + closeDetails: 'Schriftartdetails schließen', + unavailable: 'Office-Vorschauen sind nicht verfügbar. Aktivieren Sie den Dokumentvorschaudienst auf dem Computer, auf dem DeepSeek Harness ausgeführt wird.', + invalid: 'Diese Office-Datei kann nicht in der Vorschau angezeigt werden. Möglicherweise ist es beschädigt, passwortgeschützt oder hat die falsche Erweiterung.', + tooLarge: 'Die Office-Datei oder das konvertierte PDF überschreitet die Größenbeschränkung für die Vorschau. Reduzieren Sie die Dateigröße oder passen Sie die Vorschaukonfiguration an.', + failed: 'Die Office-Konvertierung ergab kein brauchbares PDF. Überprüfen Sie die Datei und versuchen Sie es erneut.', + timeout: 'Zeitüberschreitung bei der Office-Konvertierung. Versuchen Sie es erneut.', + busy: 'Die Office-Vorschau ist beschäftigt. Versuchen Sie es in Kürze noch einmal.', + changed: 'Die Datei wurde beim Lesen geändert. Öffnen Sie die Vorschau erneut.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + title: 'Офисный документ', + loading: 'Оформление документа...', + retry: 'Повторить', + viewMissingFonts: 'Отсутствуют шрифты: {count}. Нажмите, чтобы просмотреть.', + missingFontsTitle: 'Отсутствуют шрифты', + missingFontsDescription: 'Эти шрифты недоступны для этой предварительной версии. Текст и оформление могут отличаться от исходного документа.', + missingFontsCount: 'Шрифты: {count}', + closeDetails: 'Закрыть сведения о шрифте', + unavailable: 'Предварительные версии Office недоступны. Включите службу предварительного просмотра документов на компьютере под управлением DeepSeek Harness.', + invalid: 'Этот файл Office нельзя просмотреть. Возможно, он поврежден, защищен паролем или имеет неправильное расширение.', + tooLarge: 'Размер файла Office или преобразованного PDF-файла превышает ограничение на размер предварительного просмотра. Уменьшите размер файла или настройте конфигурацию предварительного просмотра.', + failed: 'Преобразование Office не привело к созданию пригодного для использования PDF-файла. Проверьте файл и повторите попытку.', + timeout: 'Время преобразования Office истекло. Попробуйте еще раз.', + busy: 'Предварительная версия Office занята. Повторите попытку через некоторое время.', + changed: 'Файл изменился во время чтения. Снова откройте предварительный просмотр.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + title: 'Documento de escritório', + loading: 'Renderizando documento...', + retry: 'Tentar novamente', + viewMissingFonts: 'Fontes ausentes: {count}. Clique para visualizar.', + missingFontsTitle: 'Fontes ausentes', + missingFontsDescription: 'Essas fontes não estão disponíveis para esta visualização. O texto e o layout podem diferir do documento original.', + missingFontsCount: 'Fontes: {count}', + closeDetails: 'Fechar detalhes da fonte', + unavailable: 'As visualizações do Office não estão disponíveis. Ative o serviço de visualização de documentos no computador que executa DeepSeek Harness.', + invalid: 'Este arquivo do Office não pode ser visualizado. Ele pode estar danificado, protegido por senha ou ter a extensão errada.', + tooLarge: 'O arquivo Office ou PDF convertido excede o limite de tamanho de visualização. Reduza o tamanho do arquivo ou ajuste a configuração de visualização.', + failed: 'A conversão do Office não produziu um PDF utilizável. Verifique o arquivo e tente novamente.', + timeout: 'A conversão do Office expirou. Tente novamente.', + busy: 'A visualização do Office está ocupada. Tente novamente em breve.', + changed: 'O arquivo foi alterado durante a leitura. Reabra a visualização.', +} satisfies Record diff --git a/packages/client/ui-sidebar-documentpreview/src/client/pdf/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/pdf/index.ts index ee099bb22a..c49e52b683 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/pdf/index.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/pdf/index.ts @@ -9,7 +9,7 @@ import type { BoundActions } from '@deepseek-ai/dsh-client-store' import type { SessionId } from '@deepseek-ai/dsh-session/types' import { createPdfStore, type PdfStore } from './store.ts' import { ZoomViewport, zoomSurfaceClass } from '../zoom/ZoomViewport.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' /** PDF metadata and keyed body share this package-local implementation identity. */ export const PDF_BODY_ID = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/pdf' @@ -25,7 +25,7 @@ export function pdfBodyDefinition(title: () => string): DocumentPreviewDefinitio /** @param ctx - context carrying the locale, document registry, and slot registry. */ export function apply(ctx: Context): void { - ctx.effect(() => ctx.locale.register('sidebarPdf', { zh, en, ja })) + ctx.effect(() => ctx.locale.register('sidebarPdf', { zh, en, ja, es, fr, de, ru, pt })) const t = ctx.locale.bind('sidebarPdf') ctx.effect(() => ctx.documentPreviews.register(pdfBodyDefinition(() => t('title')))) const presentation = pdfBodyRegistration(ctx) diff --git a/packages/client/ui-sidebar-documentpreview/src/client/pdf/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/pdf/locales.ts index e9de97b4d3..55722d8888 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/pdf/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/pdf/locales.ts @@ -1,4 +1,4 @@ -import { zoomEn, zoomJa, zoomZh } from '../zoom/locales.ts' +import { zoomEn, zoomJa, zoomEs, zoomFr, zoomDe, zoomRu, zoomPt, zoomZh } from '../zoom/locales.ts' /** Copy owned by the PDF renderer. */ export const zh = { @@ -51,3 +51,73 @@ export const ja = { 'unsupported': 'PDFのプレビューには完全なファイル内容が必要です。', 'retry': '再試行', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + ...zoomEs, + title: 'PDF', + pageImage: 'Página PDF {page}', + loading: 'Representando documento...', + rendering: 'Página de renderizado...', + failed: 'No se puede mostrar PDF: {message}', + password: 'Este PDF requiere una contraseña; No se admiten vistas previas protegidas con contraseña.', + workerFailed: 'El proceso de renderizado de PDF no pudo continuar. Vuelva a intentarlo.', + unsupported: 'La vista previa de PDF requiere el contenido completo del archivo.', + retry: 'Reintentar', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + ...zoomFr, + title: 'PDF', + pageImage: 'Page PDF {page}', + loading: 'Rendu du document...', + rendering: 'Page de rendu…', + failed: "Impossible d'afficher le PDF : {message}", + password: 'Ce PDF nécessite un mot de passe ; les aperçus protégés par mot de passe ne sont pas pris en charge.', + workerFailed: "Le processus de rendu PDF n'a pas pu continuer. Veuillez réessayer.", + unsupported: "L'aperçu PDF nécessite le contenu complet du fichier.", + retry: 'Réessayer', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + ...zoomDe, + title: 'PDF', + pageImage: 'PDF-Seite {page}', + loading: 'Dokument wird gerendert...', + rendering: 'Rendering-Seite…', + failed: 'PDF kann nicht angezeigt werden: {message}', + password: 'Für dieses PDF ist ein Passwort erforderlich. Passwortgeschützte Vorschauen werden nicht unterstützt.', + workerFailed: 'Der PDF-Rendering-Prozess konnte nicht fortgesetzt werden. Bitte versuchen Sie es erneut.', + unsupported: 'Für die PDF-Vorschau ist der vollständige Dateiinhalt erforderlich.', + retry: 'Erneut versuchen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + ...zoomRu, + title: 'PDF', + pageImage: 'PDF-страница {page}', + loading: 'Оформление документа...', + rendering: 'Отображение страницы…', + failed: 'Невозможно отобразить PDF: {message}', + password: 'Для этого PDF-файла требуется пароль; предварительный просмотр, защищенный паролем, не поддерживается.', + workerFailed: 'Процесс рендеринга PDF не может быть продолжен. Пожалуйста, повторите попытку.', + unsupported: 'Для предварительного просмотра PDF требуется полное содержимое файла.', + retry: 'Повторить', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + ...zoomPt, + title: 'PDF', + pageImage: 'Página PDF {page}', + loading: 'Renderizando documento...', + rendering: 'Página de renderização…', + failed: 'Não é possível exibir PDF: {message}', + password: 'Este PDF requer uma senha; visualizações protegidas por senha não são suportadas.', + workerFailed: 'O processo de renderização do PDF não pôde continuar. Por favor, tente novamente.', + unsupported: 'A visualização do PDF requer o conteúdo completo do arquivo.', + retry: 'Tentar novamente', +} satisfies Record diff --git a/packages/client/ui-sidebar-documentpreview/src/client/zoom/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/zoom/locales.ts index ce263fccfd..6edb1573b4 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/zoom/locales.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/zoom/locales.ts @@ -30,3 +30,53 @@ export const zoomJa = { 'zoomFitWidth': '幅に合わせる', 'zoomValue': '{percent}%', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const zoomEs = { + zoomControls: 'Controles de zoom', + zoomMenu: 'Elige zoom', + zoomOut: 'alejar', + zoomIn: 'Acercar', + zoomFitWidth: 'Ancho de ajuste', + zoomValue: '{percent}%', +} satisfies typeof zoomEn + +/** French dictionary, checked against the English keys. */ +export const zoomFr = { + zoomControls: 'Commandes de zoom', + zoomMenu: 'Choisissez le zoom', + zoomOut: 'Zoom arrière', + zoomIn: 'Zoomer', + zoomFitWidth: 'Ajuster la largeur', + zoomValue: '{percent}%', +} satisfies typeof zoomEn + +/** German dictionary, checked against the English keys. */ +export const zoomDe = { + zoomControls: 'Zoom-Steuerelemente', + zoomMenu: 'Wählen Sie Zoom', + zoomOut: 'Herauszoomen', + zoomIn: 'Vergrößern', + zoomFitWidth: 'Breite anpassen', + zoomValue: '{percent}%', +} satisfies typeof zoomEn + +/** Russian dictionary, checked against the English keys. */ +export const zoomRu = { + zoomControls: 'Элементы управления масштабированием', + zoomMenu: 'Выберите масштаб', + zoomOut: 'Уменьшить масштаб', + zoomIn: 'Увеличить масштаб', + zoomFitWidth: 'Подходит по ширине', + zoomValue: '{percent}%', +} satisfies typeof zoomEn + +/** Portuguese dictionary, checked against the English keys. */ +export const zoomPt = { + zoomControls: 'Controles de zoom', + zoomMenu: 'Escolha o zoom', + zoomOut: 'Diminuir zoom', + zoomIn: 'Ampliar', + zoomFitWidth: 'Ajustar largura', + zoomValue: '{percent}%', +} satisfies typeof zoomEn diff --git a/packages/client/ui-sidebar-documentpreview/tests/apply.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/apply.client.spec.ts index e1f236c578..b1a8caac0a 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/apply.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/apply.client.spec.ts @@ -31,7 +31,7 @@ import { LazyPdfBody } from '../src/client/pdf/LazyPdfBody.tsx' import { LazyExcelBody } from '../src/client/excel/LazyExcelBody.tsx' import { PDF_BODY_ID } from '../src/client/pdf/index.ts' import { CodeBody } from '../src/client/code/CodeBody.tsx' -import { en, ja, zh } from '../src/client/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/locales.ts' import type { textFace } from '../src/client/face.ts' import type { TextStore } from '../src/client/store.ts' import { FILE, SESSION, TAB_ID, byteResult, createResources, page } from './fixtures.client.ts' @@ -86,7 +86,7 @@ describe('ui-sidebar-documentpreview apply', () => { const { tabs, registered, dictionaries } = await boot() expect(tabs.get(TEXTPREVIEW_KIND)?.priority).toBe('fallback') expect(tabs.get(TEXTPREVIEW_KIND)?.id).toBe(TEXTPREVIEW_ID) - expect(dictionaries.get('sidebarDocumentPreview')).toEqual({ zh, en, ja }) + expect(dictionaries.get('sidebarDocumentPreview')).toEqual({ zh, en, ja, es, fr, de, ru, pt }) // The seat key is the implementation's id, not the kind: an extension may // take the kind over, and the seat must still find this body. expect(registered.map(entry => [entry.name, entry.key, entry.locale, entry.component])).toEqual([ diff --git a/packages/client/ui-sidebar-documentpreview/tests/excel-registration.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/excel-registration.client.spec.ts index b5df1bdaaf..9d8feb861b 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/excel-registration.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/excel-registration.client.spec.ts @@ -6,7 +6,7 @@ import { binaryDocumentPath, DocumentPreviewRegistry } from '../src/client/docum import { Config } from '../src/config.ts' import { apply } from '../src/client/excel/index.ts' import { LazyExcelBody } from '../src/client/excel/LazyExcelBody.tsx' -import { en, ja, zh } from '../src/client/excel/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/excel/locales.ts' it('registers complete binary reads and removes the slot and locale on disposal', async () => { const ctx = new Context() @@ -28,7 +28,7 @@ it('registers complete binary reads and removes the slot and locale on disposal' expect(candidate.title()).toBe('Spreadsheet') expect(binaryDocumentPath(registry.getSnapshot(), path)).toBe(/\.(xlsx|xls)$/iu.test(path)) } - expect(registerLocale).toHaveBeenCalledWith('sidebarExcel', { zh, en, ja }) + expect(registerLocale).toHaveBeenCalledWith('sidebarExcel', { zh, en, ja, es, fr, de, ru, pt }) expect(register).toHaveBeenCalledOnce() expect(register.mock.calls[0]![1]).toBe(LazyExcelBody) const options = register.mock.calls[0]![0] as { inject: () => unknown } diff --git a/packages/client/ui-sidebar-documentpreview/tests/html-apply.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/html-apply.client.spec.ts index a77c9b7c6f..eaf1b73f01 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/html-apply.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/html-apply.client.spec.ts @@ -7,7 +7,7 @@ import { sessionFileAddress } from '@deepseek-ai/dsh-util-workspace-path' import { DocumentPreviewRegistry } from '../src/client/document/registry.ts' import { apply, HTML_BODY_ID, htmlBodyDefinition } from '../src/client/html/index.ts' import { HtmlBody } from '../src/client/html/HtmlBody.tsx' -import { en, ja, zh } from '../src/client/html/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/html/locales.ts' import type { HtmlBodyProps } from '../src/client/html/HtmlBody.tsx' type Registration = { @@ -54,7 +54,7 @@ describe('HTML registration', () => { await fiber.await() expect(registry.candidates('INDEX.HTM').map(entry => entry.id)).toEqual([HTML_BODY_ID]) expect(registry.getSnapshot()[0]?.title()).toBe(en.title) - expect(dictionaries.get('documentHtml')).toEqual({ zh, en, ja }) + expect(dictionaries.get('documentHtml')).toEqual({ zh, en, ja, es, fr, de, ru, pt }) expect(register).toHaveBeenCalledOnce() const registration = register.mock.calls[0]?.[0] expect(registration).toMatchObject({ name: 'sidebar.right.tab.document', key: HTML_BODY_ID, locale: 'documentHtml' }) diff --git a/packages/client/ui-sidebar-documentpreview/tests/image-registration.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/image-registration.client.spec.ts index 63e428e35f..dfacf0e27d 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/image-registration.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/image-registration.client.spec.ts @@ -6,7 +6,7 @@ import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit' import { DocumentPreviewRegistry } from '../src/client/document/registry.ts' import { ImageBody } from '../src/client/image/ImageBody.tsx' import { apply, BINARY_IMAGE_EXTENSIONS, IMAGE_BODY_ID, IMAGE_EXTENSIONS, imageBodyDefinition } from '../src/client/image/index.ts' -import { en, ja, zh } from '../src/client/image/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/image/locales.ts' import type { ZoomInjected, ZoomStore } from '../src/client/zoom/store.ts' let dispose: (() => Promise) | undefined @@ -64,7 +64,7 @@ describe('image registration', () => { expect(registry.candidates(`ASSET.${extension}`).map(entry => entry.id)).toEqual([IMAGE_BODY_ID]) } expect(registry.getSnapshot()[0]?.title()).toBe(en.title) - expect(dictionaries.get('sidebarImage')).toEqual({ zh, en, ja }) + expect(dictionaries.get('sidebarImage')).toEqual({ zh, en, ja, es, fr, de, ru, pt }) expect(register).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ name: 'sidebar.right.tab.document', key: IMAGE_BODY_ID, locale: 'sidebarImage', }), ImageBody) diff --git a/packages/client/ui-sidebar-documentpreview/tests/office-registration.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/office-registration.client.spec.ts index 334beb3da1..7e30ba921a 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/office-registration.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/office-registration.client.spec.ts @@ -13,7 +13,7 @@ import { Config } from '../src/config.ts' import { OfficeBody, type OfficeBodyInjected } from '../src/client/office/OfficeBody.tsx' import type { OfficeStore } from '../src/client/office/store.ts' import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit' -import { en, ja, zh } from '../src/client/office/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/office/locales.ts' import { en as documentEn } from '../src/client/locales.ts' vi.mock('../src/client/office/face.ts', async (importOriginal) => { @@ -91,7 +91,7 @@ async function harness(config: Partial = {}, missing?: 'remote it.each(['remote', 'render', 'files'] as const)('keeps Word and PowerPoint registration and guidance when %s is absent', async (missing) => { const h = await harness(undefined, missing) try { - expect(h.locale.register).toHaveBeenCalledWith('sidebarOffice', { zh, en, ja }) + expect(h.locale.register).toHaveBeenCalledWith('sidebarOffice', { zh, en, ja, es, fr, de, ru, pt }) for (const path of ['a.DOC', 'b.DOCX', 'c.PPT', 'd.pptx']) { expect(h.registry.candidates(path)[0]!.binaryExtensions).toEqual(['doc', 'docx', 'ppt', 'pptx']) expect(h.registry.candidates(path)[0]!.title()).toBe(en.title) diff --git a/packages/client/ui-sidebar-documentpreview/tests/pdf-registration.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/pdf-registration.client.spec.ts index a55c457e4c..965269c977 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/pdf-registration.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/pdf-registration.client.spec.ts @@ -16,7 +16,7 @@ const renderedPdf = vi.hoisted(() => vi.fn(() => null)) vi.mock('../src/client/pdf/pdf.tsx', () => ({ PdfBody: renderedPdf })) import { apply, PDF_BODY_ID } from '../src/client/pdf/index.ts' import { LazyPdfBody } from '../src/client/pdf/LazyPdfBody.tsx' -import { en, ja, zh } from '../src/client/pdf/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/pdf/locales.ts' afterEach(() => { cleanup(); renderedPdf.mockClear() }) @@ -58,7 +58,7 @@ describe('PDF registration', () => { { id: PDF_BODY_ID, extensions: ['pdf'], priority: 'builtin', loading: 'bytes-complete', wrap: false }, ]) expect(previews.candidates('report.pdf')[0]!.title()).toBe('PDF') - expect(dictionaries.get('sidebarPdf')).toEqual({ zh, en, ja }) + expect(dictionaries.get('sidebarPdf')).toEqual({ zh, en, ja, es, fr, de, ru, pt }) expect(entries[0]).toMatchObject({ name: 'sidebar.right.tab.document', key: PDF_BODY_ID, locale: 'sidebarPdf' }) const instance = entries[0]!.store.create() const face = entries[0]!.inject('s1' as SessionId, instance.actions) diff --git a/packages/client/ui-sidebar-files/locale/de.json b/packages/client/ui-sidebar-files/locale/de.json new file mode 100644 index 0000000000..996a4a4a75 --- /dev/null +++ b/packages/client/ui-sidebar-files/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Bedienfeld zum Durchsuchen von Dateien", + "description": "Durchsuchen Sie das Arbeitsbereichsverzeichnis und öffnen Sie die Datei im rechten Bereich." + } +} diff --git a/packages/client/ui-sidebar-files/locale/es.json b/packages/client/ui-sidebar-files/locale/es.json new file mode 100644 index 0000000000..d50b1818de --- /dev/null +++ b/packages/client/ui-sidebar-files/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Panel de exploración de archivos", + "description": "Explore el directorio del espacio de trabajo y abra el archivo en el panel derecho." + } +} diff --git a/packages/client/ui-sidebar-files/locale/fr.json b/packages/client/ui-sidebar-files/locale/fr.json new file mode 100644 index 0000000000..7d3f2a9917 --- /dev/null +++ b/packages/client/ui-sidebar-files/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Panneau de navigation dans les fichiers", + "description": "Parcourez le répertoire de l'espace de travail et ouvrez le fichier dans le panneau de droite." + } +} diff --git a/packages/client/ui-sidebar-files/locale/pt.json b/packages/client/ui-sidebar-files/locale/pt.json new file mode 100644 index 0000000000..27cf88ec0d --- /dev/null +++ b/packages/client/ui-sidebar-files/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Painel de navegação de arquivos", + "description": "Navegue no diretório do espaço de trabalho e abra o arquivo no painel direito." + } +} diff --git a/packages/client/ui-sidebar-files/locale/ru.json b/packages/client/ui-sidebar-files/locale/ru.json new file mode 100644 index 0000000000..8376a67afe --- /dev/null +++ b/packages/client/ui-sidebar-files/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Панель просмотра файлов", + "description": "Просмотрите каталог рабочей области и откройте файл на правой панели." + } +} diff --git a/packages/client/ui-sidebar-files/src/client/index.ts b/packages/client/ui-sidebar-files/src/client/index.ts index 0b140c0e4e..0a03f63f3d 100644 --- a/packages/client/ui-sidebar-files/src/client/index.ts +++ b/packages/client/ui-sidebar-files/src/client/index.ts @@ -20,7 +20,7 @@ import { FILES_ID, filesDefinition } from './definition.tsx' import { createList, createWatch, filesFace } from './face.ts' import { FilesBody } from './FilesBody.tsx' import { FilesTitle } from './FilesTitle.tsx' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' import { createFilesStore } from './store.ts' export type { SidebarFilesKey } from './locales.ts' @@ -79,7 +79,7 @@ export function apply(ctx: ClientContext): void { }), 'ui-sidebar-files: shortcut') }) ctx.effect(() => ctx.sidebarRightTabs.register(filesDefinition(t)), 'ui-sidebar-files: files type') - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-sidebar-files: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-sidebar-files: dictionaries') const store = createFilesStore() const inject = filesFace(createList(ctx.remote), createWatch(ctx.remote)) diff --git a/packages/client/ui-sidebar-files/src/client/locales.ts b/packages/client/ui-sidebar-files/src/client/locales.ts index 69bf81f8ce..f646b7ea74 100644 --- a/packages/client/ui-sidebar-files/src/client/locales.ts +++ b/packages/client/ui-sidebar-files/src/client/locales.ts @@ -83,3 +83,108 @@ export const ja = { 'error.notDirectory': 'それはディレクトリではありません。', 'error.unavailable': '読み取りに失敗しました: {message}', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'shortcut.noSession': 'Seleccione una sesión primero', + 'type.label': 'Archivos', + 'guide.title': 'Archivos de espacio de trabajo', + 'guide.description': 'Explorar archivos en el espacio de trabajo de esta sesión', + loading: 'Leyendo…', + empty: 'directorio vacio', + truncated: 'Demasiadas entradas y solo se muestran algunas de ellas.', + noWorkspace: 'Esta sesión no tiene ningún directorio de espacio de trabajo.', + reload: 'recargar', + autoRefresh: 'Actualización automática', + 'autoRefresh.enable': 'Habilitar actualización automática', + 'autoRefresh.disable': 'Deshabilitar la actualización automática', + 'entry.other': 'No es un archivo ni un directorio, por lo que no se puede abrir.', + 'error.notFound': 'Ese directorio desapareció. Es posible que se haya movido o eliminado.', + 'error.outsideWorkspace': 'Ese directorio está fuera del espacio de trabajo, por lo que la barra lateral no lo leerá.', + 'error.notDirectory': 'Eso no es un directorio.', + 'error.unavailable': 'Error de lectura: {message}', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'shortcut.noSession': "Sélectionnez d'abord une session", + 'type.label': 'Fichiers', + 'guide.title': "Fichiers de l'espace de travail", + 'guide.description': "Parcourir les fichiers dans l'espace de travail de cette session", + loading: 'Lecture…', + empty: 'Répertoire vide', + truncated: "Trop d'entrées, n'en affichant que certaines.", + noWorkspace: "Cette session n'a pas de répertoire d'espace de travail.", + reload: 'Recharger', + autoRefresh: 'Actualisation automatique', + 'autoRefresh.enable': "Activer l'actualisation automatique", + 'autoRefresh.disable': "Désactiver l'actualisation automatique", + 'entry.other': "Il ne s'agit pas d'un fichier ou d'un répertoire, il ne peut donc pas être ouvert.", + 'error.notFound': 'Ce répertoire a disparu. Il a peut-être été déplacé ou supprimé.', + 'error.outsideWorkspace': 'Ce répertoire se trouve en dehors de l’espace de travail, donc la barre latérale ne le lira pas.', + 'error.notDirectory': "Ce n'est pas un répertoire.", + 'error.unavailable': 'Échec de lecture : {message}', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'shortcut.noSession': 'Wählen Sie zunächst eine Sitzung aus', + 'type.label': 'Dateien', + 'guide.title': 'Arbeitsbereichsdateien', + 'guide.description': 'Durchsuchen Sie die Dateien im Arbeitsbereich dieser Sitzung', + loading: 'Lesen…', + empty: 'Leeres Verzeichnis', + truncated: 'Zu viele Einträge, es werden nur einige davon angezeigt.', + noWorkspace: 'Diese Sitzung hat kein Arbeitsbereichsverzeichnis.', + reload: 'Neu laden', + autoRefresh: 'Automatische Aktualisierung', + 'autoRefresh.enable': 'Aktivieren Sie die automatische Aktualisierung', + 'autoRefresh.disable': 'Deaktivieren Sie die automatische Aktualisierung', + 'entry.other': 'Keine Datei oder kein Verzeichnis, daher kann sie nicht geöffnet werden.', + 'error.notFound': 'Dieses Verzeichnis ist weg. Möglicherweise wurde es verschoben oder gelöscht.', + 'error.outsideWorkspace': 'Dieses Verzeichnis liegt außerhalb des Arbeitsbereichs und wird daher von der Seitenleiste nicht gelesen.', + 'error.notDirectory': 'Das ist kein Verzeichnis.', + 'error.unavailable': 'Lesen fehlgeschlagen: {message}', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'shortcut.noSession': 'Сначала выберите сеанс', + 'type.label': 'Файлы', + 'guide.title': 'Файлы рабочей области', + 'guide.description': 'Просмотр файлов в рабочей области этого сеанса', + loading: 'Чтение…', + empty: 'Пустой каталог', + truncated: 'Слишком много записей, показаны только некоторые из них.', + noWorkspace: 'В этом сеансе нет каталога рабочей области.', + reload: 'Перезагрузить', + autoRefresh: 'Автоматическое обновление', + 'autoRefresh.enable': 'Включить автоматическое обновление', + 'autoRefresh.disable': 'Отключить автоматическое обновление', + 'entry.other': 'Это не файл и не каталог, поэтому его нельзя открыть.', + 'error.notFound': 'Этот каталог исчез. Возможно, оно было перемещено или удалено.', + 'error.outsideWorkspace': 'Этот каталог находится за пределами рабочей области, поэтому боковая панель его не читает.', + 'error.notDirectory': 'Это не каталог.', + 'error.unavailable': 'Не удалось прочитать: {message}.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'shortcut.noSession': 'Selecione uma sessão primeiro', + 'type.label': 'Arquivos', + 'guide.title': 'Arquivos do espaço de trabalho', + 'guide.description': 'Procure arquivos no espaço de trabalho desta sessão', + loading: 'Lendo…', + empty: 'Diretório vazio', + truncated: 'Muitas entradas, mostrando apenas algumas delas.', + noWorkspace: 'Esta sessão não possui diretório de espaço de trabalho.', + reload: 'Recarregar', + autoRefresh: 'Atualização automática', + 'autoRefresh.enable': 'Ativar atualização automática', + 'autoRefresh.disable': 'Desativar atualização automática', + 'entry.other': 'Não é um arquivo ou diretório, portanto não pode ser aberto.', + 'error.notFound': 'Esse diretório desapareceu. Pode ter sido movido ou excluído.', + 'error.outsideWorkspace': 'Esse diretório está fora da área de trabalho, portanto a barra lateral não o lerá.', + 'error.notDirectory': 'Isso não é um diretório.', + 'error.unavailable': 'Falha na leitura: {message}', +} satisfies Record diff --git a/packages/client/ui-sidebar-files/tests/apply.client.spec.ts b/packages/client/ui-sidebar-files/tests/apply.client.spec.ts index 003df8b934..53f5e51ebd 100644 --- a/packages/client/ui-sidebar-files/tests/apply.client.spec.ts +++ b/packages/client/ui-sidebar-files/tests/apply.client.spec.ts @@ -16,7 +16,7 @@ import { apply, inject } from '../src/client/index.ts' import { apply as hostApply } from '../src/index.ts' import { FilesBody } from '../src/client/FilesBody.tsx' import { FilesTitle } from '../src/client/FilesTitle.tsx' -import { en, ja, zh } from '../src/client/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/locales.ts' interface Recorded { name: string @@ -94,7 +94,7 @@ describe('ui-sidebar-files apply', () => { expect(definition?.title('sidebar://files')).toBe('type.label') expect(definition?.guide?.map(entry => [entry.order, entry.title(), entry.description?.()])) .toEqual([[10, 'guide.title', 'guide.description']]) - expect(dictionaries.get('sidebarFiles')).toEqual({ zh, en, ja }) + expect(dictionaries.get('sidebarFiles')).toEqual({ zh, en, ja, es, fr, de, ru, pt }) // The seat key is the implementation's id, not the kind: an extension may // take the kind over, and the seat must still find this body. expect(registered.map(entry => [entry.name, entry.key, entry.locale, entry.component])).toEqual([ diff --git a/packages/client/ui-sidebar-right/README.i18n.yaml b/packages/client/ui-sidebar-right/README.i18n.yaml index c781146562..cedfe4f4e4 100644 --- a/packages/client/ui-sidebar-right/README.i18n.yaml +++ b/packages/client/ui-sidebar-right/README.i18n.yaml @@ -36,8 +36,8 @@ en: 717b86e05b44ebdd zh: 0f4f694d0ea1d7f8 /deepseek-ai-dsh-client-ui-sidebar-right/the-guide: - en: c67935eddef999fe - zh: 0648664ded92ee82 + en: 5fcda0a3a4453b52 + zh: d7818c63bf3371f2 /deepseek-ai-dsh-client-ui-sidebar-right/copy: en: 4315a1c03ca5fef3 zh: c09a09f1d9b99107 diff --git a/packages/client/ui-sidebar-right/README.md b/packages/client/ui-sidebar-right/README.md index b5839bd05d..01fbf0a30e 100644 --- a/packages/client/ui-sidebar-right/README.md +++ b/packages/client/ui-sidebar-right/README.md @@ -114,7 +114,7 @@ Close and refresh commands resolve the focused pane and tab occurrence. Page bod Each `guide` entry has an `id` unique within its provider. The guide renders `sidebar.right.tab.guide.entry` by the active definition id, forwarding the entry id, resolved title and optional description, plus the framework `useTabInfo` hook. A provider can register a custom card under its definition id; otherwise the standard icon/title button renders. The sidebar owns width and placement, while custom components own their markup and interaction. Guide cards use the shared R20 radius, including hover fills and provider-owned split buttons. Entry identity remains stable across sorting and unrelated registrations. Unboxed shortcut text aligns at the right. The terminal shell menu sits beside its entry title. -While the root-scoped `sidebar.right.tab.guide.actions` list has registrations, a **More tools** card follows the guide cards. Its menu end-aligns to the card, is at least as wide as the card, and grows with its rows up to 480px or the viewport width minus 24px. The menu rows are that list's registrations. Each receives `entries` (cards in registry order with `id`, `label`, and `pinned`; kind `files` is never listed), `onOpen(id)`, which opens the card's tab type in the guide's place, `onPinnedChange(id, pinned)`, and `onDismiss()`, which closes the menu. The guide keeps one root list of unpinned card ids, each `kind:entry id`, in browser `localStorage` under `dsh.slot.manual-order.sidebar.right.tab.guide.list.v1`, stored as `{ "unpinned": string[] }`; the key reserves the name of a future guide list slot. The kind identifies a card instead of the provider id, so a pin choice survives an extension taking over that kind with the same entry id. Cards of kind `files` always show. Unpinning hides only the card; it closes no tab and changes neither tab-type registration nor the default page. Description density follows the visible card count. Ids of cards that are not registered stay stored; stored content other than a string array shows every card. +While the root-scoped `sidebar.right.tab.guide.actions` list has registrations, a **More tools** card follows the guide cards. Its menu end-aligns to the card, is at least as wide as the card, and grows with its rows up to 480px or the viewport width minus 24px. The menu rows are that list's registrations. Each receives `entries` (cards in registry order with `id`, `label`, and `pinned`; kind `files` is never listed), `onOpen(id)`, which opens the card's tab type in the guide's place, `onPinnedChange(id, pinned)`, and `onDismiss()`, which closes the menu. The guide keeps an in-memory list of unpinned card ids, each `kind:entry id`. Plugin reload resets pin choices; legacy localStorage values are ignored. The default More menu only offers plugin browsing and creation; pin callbacks remain available to other registrations. The kind identifies a card instead of the provider id, so a pin choice survives an extension taking over that kind with the same entry id. Cards of kind `files` always show. Unpinning hides only the card; it closes no tab and changes neither tab-type registration nor the default page. Description density follows the visible card count. Unregistered card ids remain in memory until plugin reload. A tab type can declare `multiple: true` for independent instances such as terminals. Each open receives a distinct content address, so placement and docking preserve those instances; ordinary pages retain per-pane uniqueness. diff --git a/packages/client/ui-sidebar-right/README.zh.md b/packages/client/ui-sidebar-right/README.zh.md index cfb16e3e53..96f12af1a6 100644 --- a/packages/client/ui-sidebar-right/README.zh.md +++ b/packages/client/ui-sidebar-right/README.zh.md @@ -114,7 +114,7 @@ Tab 域按(Session,Tab id)保留品牌化 occurrence id、导航、中止 每个 `guide` 入口具有 provider 内唯一的 `id`。开始页按当前生效的定义 id 渲染 `sidebar.right.tab.guide.entry`,传入入口 id、解析后的标题、可选描述,以及框架提供的 `useTabInfo` hook。provider 可按自己的定义 id 注册自定义卡片;未注册时使用默认图标与标题按钮。侧栏负责宽度和排列,自定义组件负责内部结构和交互。引导卡片统一使用 R20,包含悬停底色与提供方自有的分体按钮。排序或其他注册变化不会改变入口身份。无背景的快捷键文字右对齐。终端 Shell 菜单位于入口标题旁。 -root 作用域的 `sidebar.right.tab.guide.actions` list 有注册项时,引导卡片之后显示**更多工具**卡片。它的菜单与卡片末端对齐,不窄于卡片,并随菜单行增宽,最大为 480px 与视口宽度减 24px 中的较小值。菜单行为该 list 的注册项。每个注册项接收 `entries`(按注册表顺序的卡片,含 `id`、`label` 与 `pinned`;不含 kind 为 `files` 的卡片)、在引导页位置打开卡片标签类型的 `onOpen(id)`、`onPinnedChange(id, pinned)`,以及关闭菜单的 `onDismiss()`。引导页在浏览器 `localStorage` 的 `dsh.slot.manual-order.sidebar.right.tab.guide.list.v1` 下保存一份 root 级的已收起卡片 id 列表,每个 id 为 `kind:入口 id`,格式为 `{ "unpinned": string[] }`;该键名为以后的引导入口 list slot 预留。卡片由 kind 而不是提供方 id 标识,因此扩展接管某个 kind 且保持入口 id 时,Pin 选择仍然有效。kind 为 `files` 的卡片始终显示。收起只隐藏卡片,不关闭标签,也不改变标签类型注册或默认页。描述的显示密度按可见卡片数计算。未注册卡片的 id 保留在列表中;保存内容不是字符串数组时,所有卡片都显示。 +root 作用域的 `sidebar.right.tab.guide.actions` list 有注册项时,引导卡片之后显示**更多工具**卡片。它的菜单与卡片末端对齐,不窄于卡片,并随菜单行增宽,最大为 480px 与视口宽度减 24px 中的较小值。菜单行为该 list 的注册项。每个注册项接收 `entries`(按注册表顺序的卡片,含 `id`、`label` 与 `pinned`;不含 kind 为 `files` 的卡片)、在引导页位置打开卡片标签类型的 `onOpen(id)`、`onPinnedChange(id, pinned)`,以及关闭菜单的 `onDismiss()`。引导页在内存中保存已收起卡片 id 列表,每个 id 为 `kind:入口 id`。插件重新加载时 Pin 选择重置,不读取旧 localStorage 值。默认更多菜单只提供插件浏览和创建;其他注册项仍可使用 Pin 回调。卡片由 kind 而不是提供方 id 标识,因此扩展接管某个 kind 且保持入口 id 时,Pin 选择仍然有效。kind 为 `files` 的卡片始终显示。收起只隐藏卡片,不关闭标签,也不改变标签类型注册或默认页。描述的显示密度按可见卡片数计算。未注册卡片的 id 保留在内存中,直到插件重新加载。 终端等需要独立实例的 tab 类型可声明 `multiple: true`。每次打开都会获得独立内容地址,因此移动和停靠会保留这些实例;普通页面仍在每个格内去重。 diff --git a/packages/client/ui-sidebar-right/locale/de.json b/packages/client/ui-sidebar-right/locale/de.json new file mode 100644 index 0000000000..e94663c445 --- /dev/null +++ b/packages/client/ui-sidebar-right/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "rechtes Panel", + "description": "Verwalten Sie den Erweiterungsstatus, den Inhalt und die Navigation des rechten Bereichs." + } +} diff --git a/packages/client/ui-sidebar-right/locale/es.json b/packages/client/ui-sidebar-right/locale/es.json new file mode 100644 index 0000000000..a7d89d3719 --- /dev/null +++ b/packages/client/ui-sidebar-right/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "panel derecho", + "description": "Administre el estado de expansión, el contenido y la navegación del panel derecho." + } +} diff --git a/packages/client/ui-sidebar-right/locale/fr.json b/packages/client/ui-sidebar-right/locale/fr.json new file mode 100644 index 0000000000..39057044ef --- /dev/null +++ b/packages/client/ui-sidebar-right/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "panneau droit", + "description": "Gérez l'état d'expansion, le contenu et la navigation du panneau de droite." + } +} diff --git a/packages/client/ui-sidebar-right/locale/pt.json b/packages/client/ui-sidebar-right/locale/pt.json new file mode 100644 index 0000000000..53a65eeae1 --- /dev/null +++ b/packages/client/ui-sidebar-right/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "painel direito", + "description": "Gerencie o estado da expansão, o conteúdo e a navegação do painel direito." + } +} diff --git a/packages/client/ui-sidebar-right/locale/ru.json b/packages/client/ui-sidebar-right/locale/ru.json new file mode 100644 index 0000000000..cf55dfcadd --- /dev/null +++ b/packages/client/ui-sidebar-right/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "правая панель", + "description": "Управляйте состоянием расширения, содержимым и навигацией правой панели." + } +} diff --git a/packages/client/ui-sidebar-right/src/client/index.ts b/packages/client/ui-sidebar-right/src/client/index.ts index cb6a2f6e3f..99b800b6c8 100644 --- a/packages/client/ui-sidebar-right/src/client/index.ts +++ b/packages/client/ui-sidebar-right/src/client/index.ts @@ -42,7 +42,7 @@ import { SidebarSessionViews } from './session-views.ts' import { createSidebarRightController, type SidebarRightController } from './service.ts' import { SidebarRightTabRegistry } from './tab-registry.ts' import { createSidebarRightStore } from './stores.ts' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' import { GUIDE_ID, guideDefinition } from './tabs/guide/definition.ts' import { createGuidePins } from './tabs/guide/guide-pins.ts' import { guideTabInfoFactory, tabInfoFactory } from './tab-info.ts' @@ -161,7 +161,7 @@ export function apply(ctx: ClientContext): void { void disposeRegistry() }, 'ui-sidebar-right: service faces') - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-sidebar-right: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-sidebar-right: dictionaries') ctx.effect(() => registerSidebarShortcuts(ctx.shortcuts, controller, t, () => { void ctx.shortcuts.closeWindow().catch((error: unknown) => { console.error('Window close failed', error) }) }), 'ui-sidebar-right: shortcuts') diff --git a/packages/client/ui-sidebar-right/src/client/locales.ts b/packages/client/ui-sidebar-right/src/client/locales.ts index f6b70b8a88..1f4b190402 100644 --- a/packages/client/ui-sidebar-right/src/client/locales.ts +++ b/packages/client/ui-sidebar-right/src/client/locales.ts @@ -125,3 +125,198 @@ export const ja = { 'tab.guide.more': 'その他のツール', 'tab.unavailable': 'この種類のコンテンツを表示できるものはまだありません。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'command.close': 'Cerrar la página o ventana actual', + 'command.refresh': 'Actualizar la página actual', + 'command.noRefresh': 'Esta página no se puede actualizar', + 'command.toggle': 'Alternar barra lateral derecha', + 'command.fullscreen': 'Alternar panel de pantalla completa', + 'command.noSession': 'Seleccione una sesión primero', + 'command.noFocus': 'Enfoque primero un panel de la barra lateral derecha', + 'command.stale': 'La página cambió; enfocarlo de nuevo', + 'command.collapsed': 'Expande primero la barra lateral derecha', + 'command.float': 'Esta acción no está disponible en un panel flotante', + 'command.empty': 'Abra una página primero', + 'command.budget': 'Dos paneles es el límite', + 'command.width': 'No hay suficiente ancho para dividir, ampliar la barra lateral', + 'chrome.expand': 'Abrir barra lateral', + 'chrome.expandAria': 'Abrir barra lateral derecha', + 'chrome.collapse': 'Contraer barra lateral', + 'chrome.collapseAria': 'Contraer barra lateral derecha', + 'chrome.toFullscreen': 'Pantalla completa', + 'chrome.exitFullscreen': 'Salir de pantalla completa', + 'dock.emptyPane': 'Panel vacío', + 'dock.splitPane': 'dividir', + 'dock.splitPaneDisabled': 'Dos paneles es el límite', + 'dock.splitPaneNarrow': 'No hay suficiente ancho para dividir, ampliar la barra lateral', + 'dock.closeTab': 'Cerrar', + 'dock.addTab': 'Nueva pestaña', + 'dock.dockFloat': 'Enviar de vuelta a la barra lateral', + 'dock.closeFloat': 'Cerrar', + 'dock.drop.center': 'Muévete aquí', + 'dock.drop.left': 'Agregar división izquierda', + 'dock.drop.right': 'Agregar división a la derecha', + 'dock.drop.top': 'Agregar división superior', + 'dock.drop.bottom': 'Agregar división inferior', + 'tab.guide.title': 'Empezar', + 'tab.guide.more': 'Más herramientas', + 'tab.unavailable': 'Nada aquí puede ver este tipo de contenido todavía.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'command.close': 'Fermer la page ou la fenêtre actuelle', + 'command.refresh': 'Actualiser la page actuelle', + 'command.noRefresh': 'Cette page ne peut pas être actualisée', + 'command.toggle': 'Basculer la barre latérale droite', + 'command.fullscreen': 'Basculer le panneau en plein écran', + 'command.noSession': "Sélectionnez d'abord une session", + 'command.noFocus': "Concentrer d'abord un volet de la barre latérale droite", + 'command.stale': 'La page a changé ; concentre-le à nouveau', + 'command.collapsed': "Développez d'abord la barre latérale droite", + 'command.float': "Cette action n'est pas disponible dans un panneau flottant", + 'command.empty': "Ouvrir d'abord une page", + 'command.budget': 'Deux panneaux sont la limite', + 'command.width': 'Pas assez de largeur pour diviser, élargir la barre latérale', + 'chrome.expand': 'Ouvrir la barre latérale', + 'chrome.expandAria': 'Ouvrir la barre latérale droite', + 'chrome.collapse': 'Réduire la barre latérale', + 'chrome.collapseAria': 'Réduire la barre latérale droite', + 'chrome.toFullscreen': 'Plein écran', + 'chrome.exitFullscreen': 'Quitter le plein écran', + 'dock.emptyPane': 'Volet vide', + 'dock.splitPane': 'Diviser', + 'dock.splitPaneDisabled': 'Deux panneaux sont la limite', + 'dock.splitPaneNarrow': 'Pas assez de largeur pour diviser, élargir la barre latérale', + 'dock.closeTab': 'Fermer', + 'dock.addTab': 'Nouvel onglet', + 'dock.dockFloat': 'Renvoyer à la barre latérale', + 'dock.closeFloat': 'Fermer', + 'dock.drop.center': 'Déplacez-vous ici', + 'dock.drop.left': 'Ajouter une division à gauche', + 'dock.drop.right': 'Ajouter une division à droite', + 'dock.drop.top': 'Ajouter une division supérieure', + 'dock.drop.bottom': 'Ajouter une division inférieure', + 'tab.guide.title': 'Commencer', + 'tab.guide.more': "Plus d'outils", + 'tab.unavailable': 'Rien ici ne peut encore afficher ce type de contenu.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'command.close': 'Aktuelle Seite oder Fenster schließen', + 'command.refresh': 'Aktuelle Seite aktualisieren', + 'command.noRefresh': 'Diese Seite kann nicht aktualisiert werden', + 'command.toggle': 'Rechte Seitenleiste umschalten', + 'command.fullscreen': 'Panel-Vollbild umschalten', + 'command.noSession': 'Wählen Sie zunächst eine Sitzung aus', + 'command.noFocus': 'Fokussieren Sie zuerst einen rechten Seitenleistenbereich', + 'command.stale': 'Die Seite hat sich geändert; Konzentrieren Sie es erneut', + 'command.collapsed': 'Erweitern Sie zunächst die rechte Seitenleiste', + 'command.float': 'Diese Aktion ist in einem schwebenden Panel nicht verfügbar', + 'command.empty': 'Öffnen Sie zunächst eine Seite', + 'command.budget': 'Zwei Scheiben sind die Grenze', + 'command.width': 'Die Breite reicht nicht aus, um die Seitenleiste zu teilen. Verbreitern Sie die Seitenleiste', + 'chrome.expand': 'Seitenleiste öffnen', + 'chrome.expandAria': 'Rechte Seitenleiste öffnen', + 'chrome.collapse': 'Seitenleiste einklappen', + 'chrome.collapseAria': 'Rechte Seitenleiste einklappen', + 'chrome.toFullscreen': 'Vollbild', + 'chrome.exitFullscreen': 'Beenden Sie den Vollbildmodus', + 'dock.emptyPane': 'Leerer Bereich', + 'dock.splitPane': 'Geteilt', + 'dock.splitPaneDisabled': 'Zwei Scheiben sind die Grenze', + 'dock.splitPaneNarrow': 'Die Breite reicht nicht aus, um die Seitenleiste zu teilen. Verbreitern Sie die Seitenleiste', + 'dock.closeTab': 'Schließen', + 'dock.addTab': 'Neuer Tab', + 'dock.dockFloat': 'Zurück zur Seitenleiste senden', + 'dock.closeFloat': 'Schließen', + 'dock.drop.center': 'Bewegen Sie sich hierher', + 'dock.drop.left': 'Linke Teilung hinzufügen', + 'dock.drop.right': 'Rechte Teilung hinzufügen', + 'dock.drop.top': 'Oberen Split hinzufügen', + 'dock.drop.bottom': 'Untere Teilung hinzufügen', + 'tab.guide.title': 'Starten', + 'tab.guide.more': 'Weitere Werkzeuge', + 'tab.unavailable': 'Diese Art von Inhalten kann hier noch nicht angezeigt werden.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'command.close': 'Закрыть текущую страницу или окно', + 'command.refresh': 'Обновить текущую страницу', + 'command.noRefresh': 'Эту страницу невозможно обновить', + 'command.toggle': 'Переключить правую боковую панель', + 'command.fullscreen': 'Переключить панель в полноэкранный режим', + 'command.noSession': 'Сначала выберите сеанс', + 'command.noFocus': 'Сначала сфокусируйтесь на правой боковой панели', + 'command.stale': 'Страница изменилась; сосредоточься еще раз', + 'command.collapsed': 'Сначала разверните правую боковую панель', + 'command.float': 'Это действие недоступно на плавающей панели.', + 'command.empty': 'Сначала откройте страницу', + 'command.budget': 'Две панели — это предел', + 'command.width': 'Недостаточно ширины для разделения, расширьте боковую панель', + 'chrome.expand': 'Открыть боковую панель', + 'chrome.expandAria': 'Открыть правую боковую панель', + 'chrome.collapse': 'Свернуть боковую панель', + 'chrome.collapseAria': 'Свернуть правую боковую панель', + 'chrome.toFullscreen': 'Полноэкранный режим', + 'chrome.exitFullscreen': 'Выйти из полноэкранного режима', + 'dock.emptyPane': 'Пустая панель', + 'dock.splitPane': 'Сплит', + 'dock.splitPaneDisabled': 'Две панели — это предел', + 'dock.splitPaneNarrow': 'Недостаточно ширины для разделения, расширьте боковую панель', + 'dock.closeTab': 'Закрыть', + 'dock.addTab': 'Новая вкладка', + 'dock.dockFloat': 'Отправить обратно на боковую панель', + 'dock.closeFloat': 'Закрыть', + 'dock.drop.center': 'Переместитесь сюда', + 'dock.drop.left': 'Добавить левый сплит', + 'dock.drop.right': 'Добавить правое разделение', + 'dock.drop.top': 'Добавить верхнее разделение', + 'dock.drop.bottom': 'Добавить нижний разрез', + 'tab.guide.title': 'Старт', + 'tab.guide.more': 'Дополнительные инструменты', + 'tab.unavailable': 'Ничто здесь пока не может просматривать такой контент.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'command.close': 'Fechar a página ou janela atual', + 'command.refresh': 'Atualizar página atual', + 'command.noRefresh': 'Esta página não pode ser atualizada', + 'command.toggle': 'Alternar barra lateral direita', + 'command.fullscreen': 'Alternar painel para tela cheia', + 'command.noSession': 'Selecione uma sessão primeiro', + 'command.noFocus': 'Foque primeiro um painel da barra lateral direita', + 'command.stale': 'A página mudou; concentre-se novamente', + 'command.collapsed': 'Expanda a barra lateral direita primeiro', + 'command.float': 'Esta ação não está disponível em um painel flutuante', + 'command.empty': 'Abra uma página primeiro', + 'command.budget': 'Dois painéis é o limite', + 'command.width': 'Largura insuficiente para dividir. Amplie a barra lateral', + 'chrome.expand': 'Abrir barra lateral', + 'chrome.expandAria': 'Abrir barra lateral direita', + 'chrome.collapse': 'Recolher barra lateral', + 'chrome.collapseAria': 'Recolher barra lateral direita', + 'chrome.toFullscreen': 'Tela cheia', + 'chrome.exitFullscreen': 'Sair da tela cheia', + 'dock.emptyPane': 'Painel vazio', + 'dock.splitPane': 'Dividir', + 'dock.splitPaneDisabled': 'Dois painéis é o limite', + 'dock.splitPaneNarrow': 'Largura insuficiente para dividir. Amplie a barra lateral', + 'dock.closeTab': 'Fechar', + 'dock.addTab': 'Nova guia', + 'dock.dockFloat': 'Enviar de volta para a barra lateral', + 'dock.closeFloat': 'Fechar', + 'dock.drop.center': 'Mova-se aqui', + 'dock.drop.left': 'Adicionar divisão à esquerda', + 'dock.drop.right': 'Adicionar divisão à direita', + 'dock.drop.top': 'Adicionar divisão superior', + 'dock.drop.bottom': 'Adicionar divisão inferior', + 'tab.guide.title': 'Começar', + 'tab.guide.more': 'Mais ferramentas', + 'tab.unavailable': 'Nada aqui pode visualizar esse tipo de conteúdo ainda.', +} satisfies Record diff --git a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx index 4d40f86e01..febe9f1954 100644 --- a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx +++ b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx @@ -19,8 +19,7 @@ * open. */ import { useState } from 'react' -import clsx from 'clsx' -import { IconAppsAddOutlineRegular, Menu, ShortcutKeys } from '@deepseek-ai/dsh-client-ui-primitives' +import { Menu, ShortcutKeys } from '@deepseek-ai/dsh-client-ui-primitives' import type { ShortcutCatalogEntry } from '@deepseek-ai/dsh-client-shortcuts/client' import type { ReactNode } from 'react' import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store' @@ -30,6 +29,7 @@ import type { import type { SidebarRightGuideActionsOwnerProps } from '../../contract/slots.ts' import type { SidebarRightGuideBox } from '../../tab-registry.ts' import { CompassGlyph, CubeGlyph } from './GuideTitle.tsx' +import { GuideMoreIcon } from './GuideMoreIcon.tsx' import { FIXED_GUIDE_KIND, guideEntryId } from './guide-pins.ts' import css from './GuideBody.module.css' @@ -39,7 +39,7 @@ export interface GuideInjected { readonly hooks: { readonly shortcuts: ObservableSnapshot readonly guideEntries: ObservableSnapshot - /** Unpinned `kind:id` card ids, persisted across reloads. */ + /** Unpinned `kind:id` card ids held in memory until plugin reload. */ readonly entryPins: ObservableSnapshot /** Whether `sidebar.right.tab.guide.actions` has registrations. */ readonly entryActions: ObservableSnapshot @@ -106,7 +106,7 @@ function GuideMoreMenu({ label, entries, onOpen, onPinnedChange, renderSlot }: G anchor={( )}> diff --git a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideMoreIcon.tsx b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideMoreIcon.tsx new file mode 100644 index 0000000000..61f7913aa3 --- /dev/null +++ b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideMoreIcon.tsx @@ -0,0 +1,15 @@ +/** Designer-supplied artwork used only by the right-sidebar More tools card. */ + +/** + * Render the More tools glyph at its authored size and ink. + * @returns Decorative 22px More tools artwork. + */ +export function GuideMoreIcon() { + return +} diff --git a/packages/client/ui-sidebar-right/src/client/tabs/guide/guide-pins.ts b/packages/client/ui-sidebar-right/src/client/tabs/guide/guide-pins.ts index 3614a6778b..f43ee7503d 100644 --- a/packages/client/ui-sidebar-right/src/client/tabs/guide/guide-pins.ts +++ b/packages/client/ui-sidebar-right/src/client/tabs/guide/guide-pins.ts @@ -1,12 +1,10 @@ -/** Persisted pin choices for the guide's tool cards. */ +/** In-memory pin choices for the guide's tool cards. */ import { createSnapshotStore, type ObservableSnapshot } from '@deepseek-ai/dsh-client-store' import type { SidebarRightGuideBox } from '../../tab-registry.ts' /** Tab kind whose card always shows and never enters the customization list. */ export const FIXED_GUIDE_KIND = 'files' -/** Root-level key reserved for the future guide list slot's per-list preferences. */ -const STORAGE_KEY = 'dsh.slot.manual-order.sidebar.right.tab.guide.list.v1' const NO_CARDS: readonly string[] = [] /** @@ -23,30 +21,23 @@ export interface GuidePins { /** Unpinned `kind:id` card ids; ids of cards that are not registered stay listed. */ readonly unpinned: ObservableSnapshot /** - * Show or hide one guide card; cards of the fixed kind are ignored. + * Pin or unpin one guide card; cards of the fixed kind are ignored. * @param id - `kind:id` card id. - * @param pinned - Whether the card shows on the guide. + * @param pinned - Whether the card is manually pinned. */ readonly setPinned: (id: string, pinned: boolean) => void } -/** Stored content is unchecked; anything but a string array leaves every card pinned. */ -function unpinnedCards(value: unknown): readonly string[] { - if (typeof value !== 'object' || value === null || !('unpinned' in value)) return NO_CARDS - const { unpinned } = value - return Array.isArray(unpinned) && unpinned.every((id): id is string => typeof id === 'string') ? unpinned : NO_CARDS -} - /** - * Create the persisted guide pin list; the snapshot keeps its reference until the list changes. - * @returns Validated unpinned card ids and the pin mutation. + * Create the in-memory guide pin list; the snapshot keeps its reference until the list changes. + * @returns Unpinned card ids and the pin mutation. */ export function createGuidePins(): GuidePins { - const store = createSnapshotStore({ unpinned: NO_CARDS }, { persist: { name: STORAGE_KEY } }) + const store = createSnapshotStore<{ readonly unpinned: readonly string[] }>({ unpinned: NO_CARDS }) return { - unpinned: { getSnapshot: () => unpinnedCards(store.getSnapshot()), subscribe: listener => store.subscribe(listener) }, + unpinned: { getSnapshot: () => store.getSnapshot().unpinned, subscribe: listener => store.subscribe(listener) }, setPinned: (id, pinned) => { - const current = unpinnedCards(store.getSnapshot()) + const current = store.getSnapshot().unpinned if (id.startsWith(`${FIXED_GUIDE_KIND}:`) || pinned !== current.includes(id)) return store.set({ unpinned: pinned ? current.filter(entry => entry !== id) : [...current, id] }) }, diff --git a/packages/client/ui-sidebar-right/tests/apply.client.spec.ts b/packages/client/ui-sidebar-right/tests/apply.client.spec.ts index 59c620cd3b..6b40b7e4cb 100644 --- a/packages/client/ui-sidebar-right/tests/apply.client.spec.ts +++ b/packages/client/ui-sidebar-right/tests/apply.client.spec.ts @@ -27,7 +27,7 @@ import { ExpandButton } from '../src/client/shell/ExpandButton.tsx' import { GuideBody } from '../src/client/tabs/guide/GuideBody.tsx' import { GuideTitle } from '../src/client/tabs/guide/GuideTitle.tsx' import { GUIDE_ID } from '../src/client/tabs/guide/definition.ts' -import { en, ja, zh } from '../src/client/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/locales.ts' const SHORTCUT_CATALOG: readonly never[] = [] @@ -124,7 +124,7 @@ describe('ui-sidebar-right apply', () => { expect(ctx.sidebarRightTabs).toBeInstanceOf(SidebarRightTabRegistry) expect(ctx.sidebarRight).toBeInstanceOf(SidebarRightController) expect('adopt' in ctx.sidebarRight).toBe(false) - expect(dictionaries.get('sidebarRight')).toEqual({ zh, en, ja }) + expect(dictionaries.get('sidebarRight')).toEqual({ zh, en, ja, es, fr, de, ru, pt }) const guide = ctx.sidebarRightTabs.get('guide') expect(guide?.id).toBe(GUIDE_ID) expect(guide?.priority).toBe('builtin') diff --git a/packages/client/ui-sidebar-right/tests/guide-body.client.spec.tsx b/packages/client/ui-sidebar-right/tests/guide-body.client.spec.tsx index 34429ce6db..82b8d386e0 100644 --- a/packages/client/ui-sidebar-right/tests/guide-body.client.spec.tsx +++ b/packages/client/ui-sidebar-right/tests/guide-body.client.spec.tsx @@ -119,6 +119,12 @@ describe('GuideBody', () => { const guide = b.view.container.querySelector('[data-sidebar-right-guide]') expect(guide?.lastElementChild?.contains(more)).toBe(true) expect(more.getAttribute('aria-haspopup')).toBe('menu') + const glyph = more.querySelector('svg') + expect(glyph?.getAttribute('width')).toBe('22') + expect(glyph?.getAttribute('height')).toBe('22') + expect(glyph?.getAttribute('stroke')).toBe('#A6B2C7') + expect(glyph?.getAttribute('aria-hidden')).toBe('true') + expect(glyph?.querySelectorAll('path')).toHaveLength(4) fireEvent.click(more) expect(more.getAttribute('aria-expanded')).toBe('true') expect(b.actionsOwner().entries).toEqual([ diff --git a/packages/client/ui-sidebar-right/tests/guide-pins.client.spec.ts b/packages/client/ui-sidebar-right/tests/guide-pins.client.spec.ts index 1bc8994b94..5c775816fd 100644 --- a/packages/client/ui-sidebar-right/tests/guide-pins.client.spec.ts +++ b/packages/client/ui-sidebar-right/tests/guide-pins.client.spec.ts @@ -1,5 +1,5 @@ // @vitest-environment jsdom -/** Guide card pins persist per `kind:id` and ignore every Files card. */ +/** Guide card pins stay in memory per `kind:id` and ignore every Files card. */ import { afterEach, describe, expect, it, vi } from 'vitest' import { createGuidePins, guideEntryId } from '../src/client/tabs/guide/guide-pins.ts' @@ -11,15 +11,15 @@ describe('guide card pins', () => { expect(guideEntryId({ kind: 'terminal', id: 'shell' })).toBe('terminal:shell') }) - it('persists unpinned card ids and restores them in a new instance', () => { + it('keeps unpinned card ids in memory without restoring them in a new instance', () => { const pins = createGuidePins() const initial = pins.unpinned.getSnapshot() expect(initial).toEqual([]) expect(pins.unpinned.getSnapshot()).toBe(initial) pins.setPinned('terminal:shell', false) pins.setPinned('browser:page', false) - expect(JSON.parse(localStorage.getItem(KEY) ?? 'null')).toEqual({ unpinned: ['terminal:shell', 'browser:page'] }) - expect(createGuidePins().unpinned.getSnapshot()).toEqual(['terminal:shell', 'browser:page']) + expect(localStorage.getItem(KEY)).toBeNull() + expect(createGuidePins().unpinned.getSnapshot()).toEqual([]) pins.setPinned('terminal:shell', true) expect(pins.unpinned.getSnapshot()).toEqual(['browser:page']) }) @@ -38,20 +38,14 @@ describe('guide card pins', () => { }) it('keeps ids of cards that are not registered', () => { - localStorage.setItem(KEY, JSON.stringify({ unpinned: ['removed:card'] })) const pins = createGuidePins() + pins.setPinned('removed:card', false) pins.setPinned('terminal:shell', false) expect(pins.unpinned.getSnapshot()).toEqual(['removed:card', 'terminal:shell']) }) - it.each([ - ['a non-array list', { unpinned: 'terminal:shell' }], - ['non-string ids', { unpinned: [false] }], - ['a missing list', {}], - ['null', null], - ['a number', 7], - ])('reads %s as no unpinned cards with a stable snapshot', (_name, stored) => { - localStorage.setItem(KEY, JSON.stringify(stored)) + it('ignores a legacy stored list instead of restoring it', () => { + localStorage.setItem(KEY, JSON.stringify({ unpinned: ['terminal:shell'] })) const pins = createGuidePins() const snapshot = pins.unpinned.getSnapshot() expect(snapshot).toEqual([]) diff --git a/packages/client/ui-sidebar-terminal/locale/de.json b/packages/client/ui-sidebar-terminal/locale/de.json new file mode 100644 index 0000000000..bd4022653e --- /dev/null +++ b/packages/client/ui-sidebar-terminal/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Anschlussfeld", + "description": "Verwenden Sie das interaktive Befehlsterminal im rechten Bereich." + } +} diff --git a/packages/client/ui-sidebar-terminal/locale/es.json b/packages/client/ui-sidebar-terminal/locale/es.json new file mode 100644 index 0000000000..4e2351855e --- /dev/null +++ b/packages/client/ui-sidebar-terminal/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "panel de terminales", + "description": "Utilice la terminal de comando interactiva en el panel derecho." + } +} diff --git a/packages/client/ui-sidebar-terminal/locale/fr.json b/packages/client/ui-sidebar-terminal/locale/fr.json new file mode 100644 index 0000000000..a6b35dd180 --- /dev/null +++ b/packages/client/ui-sidebar-terminal/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "panneau à bornes", + "description": "Utilisez le terminal de commande interactif dans le panneau de droite." + } +} diff --git a/packages/client/ui-sidebar-terminal/locale/pt.json b/packages/client/ui-sidebar-terminal/locale/pt.json new file mode 100644 index 0000000000..373527231e --- /dev/null +++ b/packages/client/ui-sidebar-terminal/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "painel de terminais", + "description": "Use o terminal de comando interativo no painel direito." + } +} diff --git a/packages/client/ui-sidebar-terminal/locale/ru.json b/packages/client/ui-sidebar-terminal/locale/ru.json new file mode 100644 index 0000000000..8eaf7dd35e --- /dev/null +++ b/packages/client/ui-sidebar-terminal/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "терминальная панель", + "description": "Используйте интерактивный командный терминал на правой панели." + } +} diff --git a/packages/client/ui-sidebar-terminal/src/client/index.ts b/packages/client/ui-sidebar-terminal/src/client/index.ts index e355b4844c..0390d3f7b2 100644 --- a/packages/client/ui-sidebar-terminal/src/client/index.ts +++ b/packages/client/ui-sidebar-terminal/src/client/index.ts @@ -17,7 +17,7 @@ import { TerminalTitle } from './TerminalTitle.tsx' // import { TerminalRecovery, type TerminalRecoveryInjected } from './TerminalRecovery.tsx' // import { TerminalCleanup, type TerminalCleanupInjected } from './TerminalCleanup.tsx' import type { TerminalBodyInjected, TerminalInjected } from './face.ts' -import { en, ja, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh } from './locales.ts' /** Services needed by the terminal's two sidebar seats. */ export const inject = ['slots', 'locale', 'sidebarRight', 'sidebarRightTabs', 'webTerminals', 'theme', 'shortcuts'] @@ -67,7 +67,7 @@ export function apply(ctx: Context): void { return { status: 'handled', run: () => { ctx.sidebarRight.openTabFromTarget('terminal', target) } } }, }), 'ui-sidebar-terminal: shortcut') - ctx.effect(() => ctx.locale.register(namespace, { zh, en, ja }), 'ui-sidebar-terminal.copy') + ctx.effect(() => ctx.locale.register(namespace, { zh, en, ja, es, fr, de, ru, pt }), 'ui-sidebar-terminal.copy') ctx.effect(() => ctx.sidebarRightTabs.register({ id, kind: 'terminal', multiple: true, priority: 'builtin', title: () => t('title'), guide: [{ id: 'new', order: 20, title: () => t('new'), description: () => t('description'), icon: PluginArtworkTerminal }], diff --git a/packages/client/ui-sidebar-terminal/src/client/locales.ts b/packages/client/ui-sidebar-terminal/src/client/locales.ts index 7c822d6216..28059f5876 100644 --- a/packages/client/ui-sidebar-terminal/src/client/locales.ts +++ b/packages/client/ui-sidebar-terminal/src/client/locales.ts @@ -74,3 +74,93 @@ export const ja = { 'invalidOutput': 'ターミナル画面を受信できませんでした。再接続して復元してください。', 'terminalLimit': 'ターミナルの上限に達しました。未使用のターミナルを閉じて再試行してください。終了したターミナルも上限に含まれます。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'shortcut.noSession': 'Seleccione una sesión primero', + recoveryFailed: 'Falló la recuperación del terminal: {message}', retryRecovery: 'Reintentar la recuperación del terminal', + shell: 'Elige concha', shellLoading: 'Cargando proyectiles…', shellEmpty: 'No hay conchas disponibles', description: 'Ejecutar comandos en el espacio de trabajo de sesión', + title: 'terminales', new: 'Nueva terminal', loading: 'Entorno de terminal de lectura...', creating: 'Empezando…', + connecting: 'Conectando…', disconnected: 'Desconectado.', reconnect: 'Reconectar', + readonly: 'Esta vista es de sólo lectura.', control: 'toma el control', + closed: 'Terminal cerrada.', exited: 'Proceso cerrado ({code})', failed: 'Error de terminal: {message}', + rename: 'Nombre del terminal', unavailable: 'No disponible', retry: 'Reintentar', + cleanupFailed: 'No se pudo finalizar el terminal “{title}”: {message}', + missingTerminal: 'Esta terminal ya no existe. Abra una nueva terminal.', + inputFull: 'El búfer de entrada está lleno. Vuelve a conectarte y vuelve a intentarlo.', + attachmentEnded: 'La conexión del terminal terminó. Vuelva a conectarse para continuar.', + invalidOutput: 'No se pudo recibir la pantalla del terminal. Vuelva a conectarse para recuperarlo.', + terminalLimit: 'Se ha alcanzado el límite terminal. Cierre los terminales no utilizados y vuelva a intentarlo. Las terminales salidas también cuentan para el límite.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'shortcut.noSession': "Sélectionnez d'abord une session", + recoveryFailed: 'Échec de la récupération du terminal : {message}', retryRecovery: 'Réessayez la récupération du terminal', + shell: 'Choisissez la coque', shellLoading: 'Chargement des obus…', shellEmpty: 'Aucune coquille disponible', description: 'Exécuter des commandes dans l’espace de travail de la session', + title: 'Borne', new: 'Nouvelle borne', loading: 'Environnement du terminal de lecture…', creating: 'Commencer…', + connecting: 'Connexion…', disconnected: 'Déconnecté.', reconnect: 'Reconnecter', + readonly: 'Cette vue est en lecture seule.', control: 'Prenez le contrôle', + closed: 'Borne fermée.', exited: 'Processus terminé ({code})', failed: 'Erreur de terminal : {message}', + rename: 'Nom du terminal', unavailable: 'Indisponible', retry: 'Réessayer', + cleanupFailed: "Le terminal « {title} » n'a pas pu être terminé : {message}", + missingTerminal: "Ce terminal n'existe plus. Ouvrez un nouveau terminal.", + inputFull: "Le tampon d'entrée est plein. Reconnectez-vous et réessayez.", + attachmentEnded: 'La connexion du terminal est terminée. Reconnectez-vous pour continuer.', + invalidOutput: "L'écran du terminal n'a pas pu être reçu. Reconnectez-vous pour le récupérer.", + terminalLimit: 'La limite terminale a été atteinte. Fermez les terminaux inutilisés et réessayez. Les terminaux sortis comptent également dans la limite.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'shortcut.noSession': 'Wählen Sie zunächst eine Sitzung aus', + recoveryFailed: 'Terminalwiederherstellung fehlgeschlagen: {message}', retryRecovery: 'Versuchen Sie die Terminalwiederherstellung erneut', + shell: 'Wählen Sie Shell', shellLoading: 'Granaten werden geladen…', shellEmpty: 'Keine Muscheln verfügbar', description: 'Führen Sie Befehle im Sitzungsarbeitsbereich aus', + title: 'Terminal', new: 'Neues Terminal', loading: 'Terminalumgebung lesen…', creating: 'Beginnend mit …', + connecting: 'Verbinden…', disconnected: 'Nicht verbunden.', reconnect: 'Wieder verbinden', + readonly: 'Diese Ansicht ist schreibgeschützt.', control: 'Übernehmen Sie die Kontrolle', + closed: 'Terminal geschlossen.', exited: 'Prozess beendet ({code})', failed: 'Terminalfehler: {message}', + rename: 'Terminalname', unavailable: 'Nicht verfügbar', retry: 'Erneut versuchen', + cleanupFailed: 'Terminal „{title}“ konnte nicht beendet werden: {message}', + missingTerminal: 'Dieses Terminal existiert nicht mehr. Öffnen Sie ein neues Terminal.', + inputFull: 'Der Eingabepuffer ist voll. Stellen Sie die Verbindung wieder her und versuchen Sie es erneut.', + attachmentEnded: 'Die Terminalverbindung wurde beendet. Stellen Sie die Verbindung wieder her, um fortzufahren.', + invalidOutput: 'Der Terminalbildschirm konnte nicht empfangen werden. Stellen Sie die Verbindung wieder her, um es wiederherzustellen.', + terminalLimit: 'Das Endlimit wurde erreicht. Schließen Sie nicht verwendete Terminals und versuchen Sie es erneut. Auch verlassene Terminals werden auf das Limit angerechnet.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'shortcut.noSession': 'Сначала выберите сеанс', + recoveryFailed: 'Восстановление терминала не удалось: {message}', retryRecovery: 'Повторить попытку восстановления терминала', + shell: 'Выбрать оболочку', shellLoading: 'Загрузка снарядов…', shellEmpty: 'Нет доступных ракушек', description: 'Запуск команд в рабочей области сеанса', + title: 'Терминал', new: 'Новый терминал', loading: 'Чтение среды терминала…', creating: 'Начало…', + connecting: 'Подключение…', disconnected: 'Отключено.', reconnect: 'Восстановить соединение', + readonly: 'Это представление доступно только для чтения.', control: 'Возьмите под свой контроль', + closed: 'Терминал закрыт.', exited: 'Процесс завершен ({code})', failed: 'Ошибка терминала: {message}', + rename: 'Имя терминала', unavailable: 'Недоступно', retry: 'Повторить', + cleanupFailed: 'Терминал «{title}» не удалось завершить: {message}', + missingTerminal: 'Этого терминала больше не существует. Откройте новый терминал.', + inputFull: 'Входной буфер заполнен. Подключитесь и повторите попытку.', + attachmentEnded: 'Терминальное соединение завершилось. Подключитесь повторно, чтобы продолжить.', + invalidOutput: 'Не удалось получить экран терминала. Повторно подключитесь, чтобы восстановить его.', + terminalLimit: 'Достигнут лимит терминала. Закройте неиспользуемые терминалы и повторите попытку. Вышедшие терминалы также учитываются в лимите.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'shortcut.noSession': 'Selecione uma sessão primeiro', + recoveryFailed: 'Falha na recuperação do terminal: {message}', retryRecovery: 'Tentar novamente a recuperação do terminal', + shell: 'Escolha a concha', shellLoading: 'Carregando conchas…', shellEmpty: 'Não há conchas disponíveis', description: 'Execute comandos no espaço de trabalho Sessão', + title: 'Terminais', new: 'Novo terminal', loading: 'Lendo o ambiente do terminal…', creating: 'Começando…', + connecting: 'Conectando…', disconnected: 'Desconectado.', reconnect: 'Reconectar', + readonly: 'Esta visualização é somente leitura.', control: 'Assuma o controle', + closed: 'Terminal fechado.', exited: 'Processo encerrado ({code})', failed: 'Erro de terminal: {message}', + rename: 'Nome do terminal', unavailable: 'Indisponível', retry: 'Tentar novamente', + cleanupFailed: 'O terminal “{title}” não pôde ser finalizado: {message}', + missingTerminal: 'Este terminal não existe mais. Abra um novo terminal.', + inputFull: 'O buffer de entrada está cheio. Reconecte e tente novamente.', + attachmentEnded: 'A conexão do terminal terminou. Reconecte para continuar.', + invalidOutput: 'A tela do terminal não pôde ser recebida. Reconecte para recuperá-lo.', + terminalLimit: 'O limite terminal foi atingido. Feche os terminais não utilizados e tente novamente. Os terminais encerrados também contam para o limite.', +} satisfies Record diff --git a/packages/client/ui-sidebar-terminal/tests/apply.client.spec.ts b/packages/client/ui-sidebar-terminal/tests/apply.client.spec.ts index 9161768ab2..618ad175cb 100644 --- a/packages/client/ui-sidebar-terminal/tests/apply.client.spec.ts +++ b/packages/client/ui-sidebar-terminal/tests/apply.client.spec.ts @@ -18,7 +18,7 @@ import { TerminalGuide, type TerminalGuideInjected } from '../src/client/Termina import { LazyTerminalBody } from '../src/client/LazyTerminalBody.tsx' import { TerminalTitle } from '../src/client/TerminalTitle.tsx' import type { TerminalBodyInjected } from '../src/client/face.ts' -import { en, ja, zh } from '../src/client/locales.ts' +import { en, ja, zh, es, fr, de, ru, pt } from '../src/client/locales.ts' vi.mock('@xterm/xterm', () => ({ Terminal: vi.fn() })) const SHORTCUT_CATALOG: readonly never[] = [] @@ -97,7 +97,7 @@ it('registers terminal views without recovery or cleanup slots, then releases co expect(renderToStaticMarkup(createElement(Icon, { size: 22 }))).toContain('width="22"') expect(renderToStaticMarkup(createElement(Icon))).toContain('width="36"') expect(definition.multiple).toBe(true) - expect(h.dictionaries.get('sidebarTerminal')).toEqual({ en, zh, ja }) + expect(h.dictionaries.get('sidebarTerminal')).toEqual({ en, zh, ja, es, fr, de, ru, pt }) expect(h.entries.map(entry => [entry.name, entry.component, entry.locale])).toEqual([ ['sidebar.right.tab.guide.entry', TerminalGuide, 'sidebarTerminal'], ['sidebar.right.pane.tab', LazyTerminalBody, 'sidebarTerminal'], diff --git a/packages/client/ui-sidebar/README.i18n.yaml b/packages/client/ui-sidebar/README.i18n.yaml index 8bf7b111e1..9f24479203 100644 --- a/packages/client/ui-sidebar/README.i18n.yaml +++ b/packages/client/ui-sidebar/README.i18n.yaml @@ -18,11 +18,11 @@ en: a970f0547496634f zh: 36ef9a85b26e19f1 /deepseek-ai-dsh-client-ui-sidebar/use-this-package/brand-and-new-session: - en: f2c2254325cea5db - zh: c9ef643b431f8328 + en: 44db2d448bc2ea6d + zh: cbb1143afb524d5b /deepseek-ai-dsh-client-ui-sidebar/use-this-package/global-panel-entries: - en: b109498d88367117 - zh: 6d407f265bacdb0d + en: e7f316023445511b + zh: 6271c8c011533877 /deepseek-ai-dsh-client-ui-sidebar/use-this-package/collapse-behavior: en: 20e744360b4be2cc zh: a90be7e8526bbb21 @@ -33,11 +33,11 @@ en: 9e7b31acee759f2b zh: 3a6beb63b23d26ae /deepseek-ai-dsh-client-ui-sidebar/use-this-package/click-analytics: - en: bb3f4a199ed49039 - zh: cc29413fd31fe2e7 + en: 8855ad013285a27e + zh: f19b3e75c38c5b0a /deepseek-ai-dsh-client-ui-sidebar/understand-the-implementation: - en: bc51c446d8b8e098 - zh: c0c7bbfb50541d33 + en: 39b5e3ca8ce8c5c2 + zh: 1d652b7554772dad /deepseek-ai-dsh-client-ui-sidebar/understand-the-implementation/slot-discipline: en: 6eefc5c45abe8a8f zh: 20b651e48a0ea903 diff --git a/packages/client/ui-sidebar/README.md b/packages/client/ui-sidebar/README.md index 049673a2ce..5a21b27987 100644 --- a/packages/client/ui-sidebar/README.md +++ b/packages/client/ui-sidebar/README.md @@ -31,13 +31,13 @@ The sidebar is the navigation shell: users see the brand, start new sessions, co ### Brand and New Session -The expanded brand row renders `sidebar.brand.mark` and `sidebar.brand.name` as independent single slots; the collapsed rail renders the same mark slot. On Web and Windows desktop, clicking the expanded brand starts a new session; hovering or focusing it shows no tooltip. Without occupants, the shell uses the fish mark and a localized local-build label. A complete build stacks a code badge below the label as `version[-commit][-dirty]`, using `DSH_CLIENT_VERSION`, the optional 7-character `DSH_CLIENT_COMMIT_HASH`, and `DSH_CLIENT_GIT_DIRTY=true`; missing version metadata omits the badge. Clicking the fallback mark or label of such a build copies that badge text instead of starting a session, also on macOS desktop, and a successful write shows a Version copied toast. New Session targets the explicit Workspace used by a scoped action, otherwise the current Session's Workspace, otherwise the most recently active Workspace; when none exists it clears into the blank New Session page. The expanded New Session button displays the effective binding as trailing grey text on keyboard focus regardless of hover capability, and on hover when the device supports it. While the shortcut is visible, the centered icon and label fade before it when space is narrow; CSS reserves the shortcut’s width without measuring the button. Icon-only controls retain tooltips with platform-formatted keycaps, including the resident macOS header control when the sidebar is hidden. All controls expose `aria-keyshortcuts`. +The expanded brand row renders `sidebar.brand.mark` and `sidebar.brand.name` as independent single slots; the collapsed rail renders the same mark slot. On Web and Windows desktop, clicking the expanded brand starts a new session; hovering or focusing it shows no tooltip. Without occupants, the shell uses the fish mark and a localized local-build label. A complete build stacks a code badge below the label as `version[-commit][-dirty]`, using `DSH_CLIENT_VERSION`, the optional 7-character `DSH_CLIENT_COMMIT_HASH`, and `DSH_CLIENT_GIT_DIRTY=true`; missing version metadata omits the badge. Clicking the fallback mark or label of such a build copies that badge text instead of starting a session, also on macOS desktop, and a successful write shows a Version copied toast. New Session targets the explicit Workspace used by a scoped action, otherwise the current Session's Workspace, otherwise the most recently active Workspace; when none exists it clears into the blank New Session page. The expanded New Session button displays the effective binding as trailing grey text on keyboard focus regardless of hover capability, and on hover when the device supports it. While the shortcut is visible, the centered icon and label fade before it when space is narrow; CSS reserves the shortcut’s width without measuring the button. Icon-only controls retain tooltips with platform-formatted keycaps, including the resident macOS header control when the sidebar is hidden. All controls expose `aria-keyshortcuts`. While the global access snapshot excludes `session/create`, the brand row stays inert and neither the New Session button nor the window-chrome New Session control renders; the Host still decides every create request. ### Global panel entries Plugins add an icon component to the root-scoped `sidebar.panellist` list with an `id`, optional `order`, and a string or locale-aware `label`. The same id addresses the component registered in the layout's root-scoped `main` keyed slot; selecting a missing main entry throws without changing the current selection. The label supplies plain visible text, the accessible name, and the collapsed tooltip. Expanded navigation rows have a 2px gap. Each row reads its own selected state through `usePanelInfo`; moving DOM focus to search or a directory picker does not change the displayed panel or its selected row. With no panel registrations, neither the list nor spacing for it is rendered. The shipped composition registers no example panel. -While the root-scoped `sidebar.panellist.actions` list has registrations, a **More** button styled as a panel row follows the rows inside the navigation landmark; the collapsed rail shows its label as a tooltip. It opens a menu whose rows are that list's registrations. Each receives `entries` (customizable panels in registration order with `id`, `label`, and `pinned`; Plugins is never listed), `onOpen(id)`, which selects the panel, `onPinnedChange(id, pinned)`, and `onDismiss()`, which closes the menu. The sidebar keeps one root list of unpinned panel ids in browser `localStorage` under `dsh.slot.manual-order.sidebar.panellist.v1`, stored as `{ "unpinned": string[] }` and shared by every Session; the rows omit unpinned panels except the selected one, which keeps its row and `aria-current` until another panel is selected, and Plugins always shows. Unpinning hides only the row; the main panel, its registration, and the current selection stay unchanged. Ids of panels that are not registered stay stored, so a panel that registers again stays unpinned; stored content other than a string array shows every row. +While the root-scoped `sidebar.panellist.actions` list has registrations, a **More** button styled as a panel row follows the rows inside the navigation landmark; the collapsed rail shows its label as a tooltip. It opens a menu whose rows are that list's registrations. Each receives `entries` (customizable panels in registration order with `id`, `label`, `pinned`, and `active`; Plugins is never listed), `onOpen(id)`, which selects the panel, `onPinnedChange(id, pinned)`, and `onDismiss()`, which closes the menu. The sidebar keeps one in-memory list of unpinned panel ids shared by every Session; Plugins always shows. The default More menu only offers plugin browsing and creation, without panel rows or pin controls. Pin callbacks remain available to other registrations, but choices reset when the plugin reloads and legacy localStorage values are ignored. Unpinning hides only the row; the main panel, its registration, and the current selection stay unchanged. Unregistered panel ids remain in the in-memory list until the plugin reloads. ### Collapse behavior @@ -64,7 +64,7 @@ On Desktop, the official navigation controls report `sidebar_menu_click` with `m - The expanded brand and the New Session button report `menu_name=new_session` with `position=sidebar`. While the Windows caption hosts the collapsed New Session button, that button reports `position=titlebar`; the macOS New Session control beside the traffic lights also reports `position=titlebar`. - The **Plugins** and **Automation tasks** rows report `menu_name=plugin` and `menu_name=cron` with `position=sidebar`. Other panel rows report nothing. - Each **More** button click reports `menu_name=more` with `position=sidebar`, whether it opens or closes the menu. -- A More menu row that opens Plugins or Automation tasks through `onOpen(id)` reports `menu_name=plugin` or `menu_name=cron` with `position=sidebar_more`. The report precedes the registration check, so a request for a panel that has unregistered still counts although nothing is selected. +- A third-party More menu row that calls `onOpen(id)` for Plugins or Automation tasks reports `menu_name=plugin` or `menu_name=cron` with `position=sidebar_more`. The shipped More menu only browses and creates plugins, so it never calls `onOpen`. The report precedes the registration check, so a request for a panel that has unregistered still counts although nothing is selected. ----- @@ -74,7 +74,7 @@ On Desktop, the official navigation controls report `sidebar_menu_click` with `m
Implementation internals — click to expand -The shell is pure composition: `SidebarRootComponentProps` composes the layout owner share, the global `useSessions` and `useWorkspaces` hooks, the declared brand, the `sidebar.workspaces` and `sidebar.settings` child slots, and injected navigation callbacks. Panel entries and their optional titles use the same composition path. Panel metadata is derived from list registrations and locale changes; selection belongs to the layout store. The injected `trackEntryClick(attributes)` forwards to `ctx.get('productAnalytics')?.track('sidebar_menu_click', attributes)`, and `selectPanel` only selects, so each control supplies its own entry and position. +The shell is pure composition: `SidebarRootComponentProps` composes the layout owner share, the global `useSessions` and `useWorkspaces` hooks, the declared brand, the `sidebar.workspaces` and `sidebar.settings` child slots, and injected navigation callbacks. The sidebar owns `sidebar.panellist` and renders each panel's registered icon directly. Panel metadata is derived from list registrations and locale changes; selection belongs to the layout store. The injected `trackEntryClick(attributes)` forwards to `ctx.get('productAnalytics')?.track('sidebar_menu_click', attributes)`, and `selectPanel` only selects, so each control supplies its own entry and position. ### Slot discipline diff --git a/packages/client/ui-sidebar/README.zh.md b/packages/client/ui-sidebar/README.zh.md index c9acb9aa78..08eb1f1648 100644 --- a/packages/client/ui-sidebar/README.zh.md +++ b/packages/client/ui-sidebar/README.zh.md @@ -31,13 +31,13 @@ dsh Web 客户端的侧边栏让用户识别当前构建、启动新会话、将 ### 品牌与 New Session -展开的品牌行把 `sidebar.brand.mark` 与 `sidebar.brand.name` 渲染为两个独立的 single slot;收起轨道则渲染同一个 mark slot。在 Web 和 Windows 桌面端,点击展开态的品牌会新建会话,悬停或聚焦时不显示 tooltip。没有占位者时,外壳使用鱼形标记和本地化的本地构建标签。完整构建会在标签下方显示代码徽标;该徽标使用 `DSH_CLIENT_VERSION`、可选的 7 位 `DSH_CLIENT_COMMIT_HASH` 与 `DSH_CLIENT_GIT_DIRTY=true` 组装成 `version[-commit][-dirty]`;缺少版本元数据时不显示徽标。此类构建中点击回退的标记或标签会复制该徽标文本而非新建会话,macOS 桌面端同样适用;写入成功后弹出“版本号复制成功”toast。New Session 优先使用作用域操作明确指定的 Workspace,否则使用当前 Session 所属 Workspace,再否则使用最近活跃 Workspace;一个 Workspace 都没有时则清空选择,进入空白 New Session 页面。 展开态的新建会话按钮在键盘聚焦时于右侧以灰色文字显示当前有效绑定,不受设备悬停能力限制;支持悬停的设备也会在悬停时显示。快捷键可见且空间较窄时,居中的图标与文字在快捷键之前渐隐;CSS 为快捷键保留宽度,无需测量按钮。纯图标控件保留使用平台键位样式的 tooltip,侧栏隐藏时常驻的 macOS 头部控件也保持一致。所有控件均提供 `aria-keyshortcuts`。 +展开的品牌行把 `sidebar.brand.mark` 与 `sidebar.brand.name` 渲染为两个独立的 single slot;收起轨道则渲染同一个 mark slot。在 Web 和 Windows 桌面端,点击展开态的品牌会新建会话,悬停或聚焦时不显示 tooltip。没有占位者时,外壳使用鱼形标记和本地化的本地构建标签。完整构建会在标签下方显示代码徽标;该徽标使用 `DSH_CLIENT_VERSION`、可选的 7 位 `DSH_CLIENT_COMMIT_HASH` 与 `DSH_CLIENT_GIT_DIRTY=true` 组装成 `version[-commit][-dirty]`;缺少版本元数据时不显示徽标。此类构建中点击回退的标记或标签会复制该徽标文本而非新建会话,macOS 桌面端同样适用;写入成功后弹出“版本号复制成功”toast。New Session 优先使用作用域操作明确指定的 Workspace,否则使用当前 Session 所属 Workspace,再否则使用最近活跃 Workspace;一个 Workspace 都没有时则清空选择,进入空白 New Session 页面。 展开态的新建会话按钮在键盘聚焦时于右侧以灰色文字显示当前有效绑定,不受设备悬停能力限制;支持悬停的设备也会在悬停时显示。快捷键可见且空间较窄时,居中的图标与文字在快捷键之前渐隐;CSS 为快捷键保留宽度,无需测量按钮。纯图标控件保留使用平台键位样式的 tooltip,侧栏隐藏时常驻的 macOS 头部控件也保持一致。所有控件均提供 `aria-keyshortcuts`。全局访问快照不含 `session/create` 时,品牌行不可点击,New Session 按钮和窗口标题栏中的 New Session 控件都不渲染;每个新建请求仍由 Host 判定。 ### 全局面板入口 插件在 root 作用域的 `sidebar.panellist` list 中注册图标组件,提供 `id`、可选 `order`,以及字符串或 locale-aware 的 `label`。同一个 id 寻址布局中 root 作用域 `main` keyed slot 的组件;选择不存在的主面板条目会抛错,并保留当前选中态。标签提供普通可见文字、无障碍名称和折叠提示。展开态导航行的间距为 2px。每一行通过 `usePanelInfo` 读取自己的选中态;DOM 焦点移到搜索框或目录选择器时,显示的面板及其列表项选中态不变。没有面板注册项时,列表及其间距均不渲染。产品随附的组合不注册示例面板。 -root 作用域的 `sidebar.panellist.actions` list 有注册项时,导航区域的面板行之后显示面板行样式的**更多**按钮;收起的窄栏以提示框显示其名称。它打开的菜单以该 list 的注册项为菜单行。每个注册项接收 `entries`(按注册顺序的可自定义面板,含 `id`、`label` 与 `pinned`;不含插件管理)、选中面板的 `onOpen(id)`、`onPinnedChange(id, pinned)`,以及关闭菜单的 `onDismiss()`。侧边栏在浏览器 `localStorage` 的 `dsh.slot.manual-order.sidebar.panellist.v1` 下保存一份 root 级的已收起面板 id 列表,格式为 `{ "unpinned": string[] }`,所有会话共用;面板行不显示已收起的面板,但当前选中的面板保留其行和 `aria-current`,直到选中其他面板;插件管理入口始终显示。收起只隐藏导航行,主面板、其注册和当前选中态不变。未注册面板的 id 保留在列表中,因此重新注册的面板保持收起;保存内容不是字符串数组时,所有行都显示。 +root 作用域的 `sidebar.panellist.actions` list 有注册项时,导航区域的面板行之后显示面板行样式的**更多**按钮;收起的窄栏以提示框显示其名称。它打开的菜单以该 list 的注册项为菜单行。每个注册项接收 `entries`(按注册顺序的可自定义面板,含 `id`、`label`、`pinned` 与 `active`;不含插件管理)、选中面板的 `onOpen(id)`、`onPinnedChange(id, pinned)`,以及关闭菜单的 `onDismiss()`。侧边栏在内存中保存所有会话共用的已收起面板 id 列表;插件管理入口始终显示。默认更多菜单只提供插件浏览和创建,不列出面板或 Pin 控件。其他注册项仍可调用 Pin 回调,但插件重新加载时选择重置,不读取旧 localStorage 值。收起只隐藏导航行,主面板、其注册和当前选中态不变。未注册面板的 id 保留在内存列表中,直到插件重新加载。 ### 折叠行为 @@ -64,7 +64,7 @@ root 作用域的 `sidebar.panellist.actions` list 有注册项时,导航区 - 展开态品牌与新建会话按钮上报 `menu_name=new_session`、`position=sidebar`。Windows 顶栏承载收起态新建会话按钮时,该按钮上报 `position=titlebar`;macOS 红绿灯旁的新建会话控件同样上报 `position=titlebar`。 - **插件**与**自动化任务**面板行分别上报 `menu_name=plugin` 与 `menu_name=cron`,`position=sidebar`。其他面板行不上报。 - 每次点击**更多**按钮都上报 `menu_name=more`、`position=sidebar`,无论打开还是关闭菜单。 -- 更多菜单中的行通过 `onOpen(id)` 打开插件或自动化任务时,上报 `menu_name=plugin` 或 `menu_name=cron`,`position=sidebar_more`。上报先于注册检查,因此所请求的面板已注销时仍会计数,但不会选中任何面板。 +- 第三方注册的更多菜单行通过 `onOpen(id)` 打开插件或自动化任务时,上报 `menu_name=plugin` 或 `menu_name=cron`,`position=sidebar_more`。随产品提供的更多菜单只浏览和创建插件,不会调用 `onOpen`。上报先于注册检查,因此所请求的面板已注销时仍会计数,但不会选中任何面板。 ----- @@ -74,7 +74,7 @@ root 作用域的 `sidebar.panellist.actions` list 有注册项时,导航区
实现细节——点击展开 -外壳是纯组合:`SidebarRootComponentProps` 组合布局 owner share、全局 `useSessions` 与 `useWorkspaces` 钩子、已声明的品牌、`sidebar.workspaces` 与 `sidebar.settings` 子 slot,以及注入的导航回调。面板入口及其可选标题使用相同的组合方式。面板元数据由列表注册和 locale 变化派生;选中态属于布局存储。注入的 `trackEntryClick(attributes)` 转发到 `ctx.get('productAnalytics')?.track('sidebar_menu_click', attributes)`;`selectPanel` 只负责选中,因此由各控件自行提供入口与位置。 +外壳是纯组合:`SidebarRootComponentProps` 组合布局 owner share、全局 `useSessions` 与 `useWorkspaces` 钩子、已声明的品牌、`sidebar.workspaces` 与 `sidebar.settings` 子 slot,以及注入的导航回调。侧边栏声明 `sidebar.panellist`,并直接渲染各面板注册的图标。面板元数据由列表注册和 locale 变化派生;选中态属于布局存储。注入的 `trackEntryClick(attributes)` 转发到 `ctx.get('productAnalytics')?.track('sidebar_menu_click', attributes)`;`selectPanel` 只负责选中,因此由各控件自行提供入口与位置。 ### slot 纪律 diff --git a/packages/client/ui-sidebar/locale/de.json b/packages/client/ui-sidebar/locale/de.json new file mode 100644 index 0000000000..b48c3793d0 --- /dev/null +++ b/packages/client/ui-sidebar/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Konversationsseitenleiste", + "description": "Konversationsliste anzeigen, Such-, Gruppierungs- und Statusaufforderungen bereitstellen." + } +} diff --git a/packages/client/ui-sidebar/locale/es.json b/packages/client/ui-sidebar/locale/es.json new file mode 100644 index 0000000000..48de49b659 --- /dev/null +++ b/packages/client/ui-sidebar/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Barra lateral de conversación", + "description": "Muestre la lista de conversaciones, proporcione indicaciones de búsqueda, agrupación y estado." + } +} diff --git a/packages/client/ui-sidebar/locale/fr.json b/packages/client/ui-sidebar/locale/fr.json new file mode 100644 index 0000000000..0dd77b45eb --- /dev/null +++ b/packages/client/ui-sidebar/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Barre latérale des conversations", + "description": "Affichez la liste des conversations, fournissez des invites de recherche, de regroupement et d’état." + } +} diff --git a/packages/client/ui-sidebar/locale/pt.json b/packages/client/ui-sidebar/locale/pt.json new file mode 100644 index 0000000000..dafc152209 --- /dev/null +++ b/packages/client/ui-sidebar/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Barra lateral de conversa", + "description": "Exibir lista de conversas, fornecer prompts de pesquisa, agrupamento e status." + } +} diff --git a/packages/client/ui-sidebar/locale/ru.json b/packages/client/ui-sidebar/locale/ru.json new file mode 100644 index 0000000000..a9df979adc --- /dev/null +++ b/packages/client/ui-sidebar/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Боковая панель беседы", + "description": "Отобразить список разговоров, предоставить поиск, группировку и подсказки о статусе." + } +} diff --git a/packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx b/packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx index 3bb05369cb..618bdd26e7 100644 --- a/packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx +++ b/packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx @@ -20,10 +20,14 @@ export type HeaderLeadingControlsProps = * controls off screen; this occupant puts them back beside the traffic * lights. The frame mounts the seat only in that state and owns its * placement, so the occupant renders unconditionally. - * @param props - Injected sidebar actions plus the sidebar locale seat. - * @returns the two window-chrome controls. + * @param props - Injected sidebar actions, the global access Hook, and the sidebar locale seat. + * @returns the sidebar toggle, plus the New Session control while the access snapshot allows `session/create`. */ -export function HeaderLeadingControls({ toggleSidebar, startSession, trackEntryClick, useShortcuts, t }: HeaderLeadingControlsProps) { +export function HeaderLeadingControls({ + toggleSidebar, startSession, trackEntryClick, useRemoteAccess, useShortcuts, t, +}: HeaderLeadingControlsProps) { + // A page whose access snapshot excludes starting a Session keeps only the sidebar toggle here. + const canStartSession = useRemoteAccess('session/create') const shortcut = useShortcuts(rows => rows.find(row => row.id === 'sidebar.left.toggle')) const newShortcut = useShortcuts(rows => rows.find(row => row.id === 'session.new')) return ( @@ -39,20 +43,22 @@ export function HeaderLeadingControls({ toggleSidebar, startSession, trackEntryC - - - + {canStartSession && ( + + + + )}
) } diff --git a/packages/client/ui-sidebar/src/client/SidebarRoot.tsx b/packages/client/ui-sidebar/src/client/SidebarRoot.tsx index cf8bcc1167..c4083d223d 100644 --- a/packages/client/ui-sidebar/src/client/SidebarRoot.tsx +++ b/packages/client/ui-sidebar/src/client/SidebarRoot.tsx @@ -135,6 +135,7 @@ export function SidebarRoot({ selectPanel, setPinned, trackEntryClick, + useRemoteAccess, usePanels, useEntryPins, useEntryActions, @@ -149,6 +150,8 @@ export function SidebarRoot({ const hasEntryActions = useEntryActions(registered => registered) const shortcut = useShortcuts(rows => rows.find(row => row.id === 'sidebar.left.toggle')) const newShortcut = useShortcuts(rows => rows.find(row => row.id === 'session.new')) + // A page whose access snapshot excludes starting a Session offers no New Session affordance. + const canStartSession = useRemoteAccess('session/create') const toggleLabel = collapsed ? t('toggle.open') : t('toggle.collapse') // Wide content stays mounted while the collapse animates (fading via // .collapsed .wide), unmounts at settle, and remounts right away on expand. @@ -278,7 +281,8 @@ export function SidebarRoot({ traffic lights and keeps the toggle at the sidebar's top-right. */} {darwinDesktop &&
{toggle}
}
- {/* Expanded, the brand doubles as a New Session shortcut — except on + {/* Expanded, the brand doubles as a New Session shortcut while the + access snapshot allows `session/create` — except on macOS, where it stays part of the logo row's window-drag surface (a button would subtract itself through the global no-drag rule); the collapsed rail's logo is the expand toggle below instead. @@ -307,7 +311,7 @@ export function SidebarRoot({ ) - return darwinDesktop + return darwinDesktop || !canStartSession ? {identity} : ( - + {canStartSession && ( + + + + )} {panels.length > 0 && (
{isExpanded && ( diff --git a/packages/client/ui-subagent/src/client/index.ts b/packages/client/ui-subagent/src/client/index.ts index 51f052672e..117b0b0605 100644 --- a/packages/client/ui-subagent/src/client/index.ts +++ b/packages/client/ui-subagent/src/client/index.ts @@ -14,7 +14,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type {} from '@deepseek-ai/dsh-client-ui-session/client' import type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client' import type {} from '@deepseek-ai/dsh-client-ui-workspace/client' -import { en, ja, NS, zh, type SubagentKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh, type SubagentKey } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { @@ -53,7 +53,7 @@ function selectReadOnlySubagent(owner: ComposerChainProps): SubagentReadOnlyMatc * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-subagent: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-subagent: dictionaries') ctx.inject(['resources', 'sidebarRightTabs'], (scope) => { registerSidebarChat(scope, ctx.locale.bind(NS)) }) diff --git a/packages/client/ui-subagent/src/client/locales.ts b/packages/client/ui-subagent/src/client/locales.ts index 9cd5f6d30e..385572dad2 100644 --- a/packages/client/ui-subagent/src/client/locales.ts +++ b/packages/client/ui-subagent/src/client/locales.ts @@ -140,3 +140,228 @@ export const ja = { 'readonly.oneShot.body': '単発タスクは追加のメッセージを受け付けません。ここで実行記録の全体を確認できます。', 'readonly.body': '親セッションがオフラインです。再度開いてメッセージの送信を続けてください。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m {seconds}s', + 'duration.days': '{days}d', + 'duration.daysHours': '{days}d {hours}h', + 'duration.months': '~{months}mo', + 'duration.monthsDays': '~{months}mo {days}d', + 'duration.years': '~{years}y', + 'duration.yearsMonths': '~{years}y {months}mo', + 'duration.exactDays': '{days}d {hours}h {minutes}m {seconds}s', + 'duration.exactTitle': 'Duración total activa: {duration}', + 'tokens.thousand': '{value}K', + 'tokens.million': '{value}M', + 'tokens.total': 'Tok {value}', + 'loading.label': 'Cargando subagentes…', + 'load.error': 'No se pueden cargar subagentes', + 'catalog.migrationRequired': 'Migración requerida', + 'retry': 'Reintentar', + 'mode.external': 'tarea externa', + 'mode.oneShot': 'un solo disparo', + 'mode.continuable': 'continuable', + 'mode.unknown': 'modo desconocido', + 'readonly.unknown.body': 'Lea la sesión secundaria para determinar si se puede continuar.', + 'activity.running': 'corriendo', + 'activity.completed': 'completado', + 'activity.inactive': 'no corriendo', + 'branch.collapse': 'Contraer descendientes de {label}', + 'branch.expand': 'Ampliar descendientes de {label}', + 'count.total.one': 'Subagente {count}', + 'count.total.other': 'Subagentes {count}', + 'count.running.one': 'Subagente {count} ejecutándose', + 'count.running.other': 'Subagentes {count} ejecutándose', + 'switcher.aria': 'Cambiar subagente: {title}', + 'tree.aria': 'Sesiones de subagente', + 'open.sidebar': 'Abrir en la barra lateral', + 'open.sidebar.aria': 'Abra {label} en la barra lateral', + 'sidebar.chat': 'Chat', + 'readonly.oneShot.title': 'Registro de subagente de una sola vez', + 'readonly.title': 'Este subagente es de solo lectura por ahora', + 'readonly.oneShot.body': 'Las tareas únicas no aceptan seguimientos; revise el registro de ejecución completo aquí.', + 'readonly.body': 'La sesión para padres está fuera de línea; Vuelva a abrirlo para continuar enviando mensajes.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m {seconds}s', + 'duration.days': '{days}d', + 'duration.daysHours': '{days}d {hours}h', + 'duration.months': '~{months}mo', + 'duration.monthsDays': '~{months}mo {days}d', + 'duration.years': '~{years}y', + 'duration.yearsMonths': '~{years}y {months}mo', + 'duration.exactDays': '{days}d {hours}h {minutes}m {seconds}s', + 'duration.exactTitle': "Durée totale d'activité : {duration}", + 'tokens.thousand': '{value}K', + 'tokens.million': '{value}M', + 'tokens.total': 'Jeton {value}', + 'loading.label': 'Chargement des sous-agents…', + 'load.error': 'Impossible de charger les sous-agents', + 'catalog.migrationRequired': 'Migration requise', + 'retry': 'Réessayer', + 'mode.external': 'Tâche externe', + 'mode.oneShot': 'un coup', + 'mode.continuable': 'continu', + 'mode.unknown': 'mode inconnu', + 'readonly.unknown.body': 'Lisez la séance enfant pour déterminer si elle peut être poursuivie.', + 'activity.running': 'courir', + 'activity.completed': 'terminé', + 'activity.inactive': 'ne fonctionne pas', + 'branch.collapse': 'Réduire les descendants de {label}', + 'branch.expand': 'Développer les descendants de {label}', + 'count.total.one': 'Sous-agent {count}', + 'count.total.other': 'Sous-agents {count}', + 'count.running.one': "Sous-agent {count} en cours d'exécution", + 'count.running.other': "Sous-agents {count} en cours d'exécution", + 'switcher.aria': 'Changer de sous-agent : {title}', + 'tree.aria': 'Sessions de sous-agents', + 'open.sidebar': 'Ouvrir dans la barre latérale', + 'open.sidebar.aria': 'Ouvrir {label} dans la barre latérale', + 'sidebar.chat': 'Discussion', + 'readonly.oneShot.title': 'Enregistrement de sous-agent unique', + 'readonly.title': 'Ce sous-agent est actuellement en lecture seule', + 'readonly.oneShot.body': 'Les tâches ponctuelles n’acceptent pas de messages de suivi ; consultez leur journal d’exécution complet ici.', + 'readonly.body': 'La session parente est hors ligne ; rouvrez-la pour continuer à envoyer des messages.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m {seconds}s', + 'duration.days': '{days}d', + 'duration.daysHours': '{days}d {hours}h', + 'duration.months': '~{months}mo', + 'duration.monthsDays': '~{months}mo {days}d', + 'duration.years': '~{years}y', + 'duration.yearsMonths': '~{years}y {months}mo', + 'duration.exactDays': '{days}d {hours}h {minutes}m {seconds}s', + 'duration.exactTitle': 'Gesamte aktive Dauer: {duration}', + 'tokens.thousand': '{value}K', + 'tokens.million': '{value}M', + 'tokens.total': '{value}-Token', + 'loading.label': 'Subagenten werden geladen…', + 'load.error': 'Subagenten können nicht geladen werden', + 'catalog.migrationRequired': 'Migration erforderlich', + 'retry': 'Erneut versuchen', + 'mode.external': 'Externe Aufgabe', + 'mode.oneShot': 'One-Shot', + 'mode.continuable': 'fortsetzbar', + 'mode.unknown': 'Unbekannter Modus', + 'readonly.unknown.body': 'Lesen Sie die untergeordnete Sitzung, um festzustellen, ob sie fortgesetzt werden kann.', + 'activity.running': 'laufen', + 'activity.completed': 'abgeschlossen', + 'activity.inactive': 'läuft nicht', + 'branch.collapse': '{label}-Nachkommen reduzieren', + 'branch.expand': 'Erweitern Sie {label}-Nachkommen', + 'count.total.one': '{count}-Subagent', + 'count.total.other': '{count}-Subagenten', + 'count.running.one': '{count}-Subagent läuft', + 'count.running.other': '{count} Subagenten laufen', + 'switcher.aria': 'Subagent wechseln: {title}', + 'tree.aria': 'Subagentensitzungen', + 'open.sidebar': 'In der Seitenleiste öffnen', + 'open.sidebar.aria': 'Öffnen Sie {label} in der Seitenleiste', + 'sidebar.chat': 'Chat', + 'readonly.oneShot.title': 'Einmaliger Subagentendatensatz', + 'readonly.title': 'Dieser Subagent ist derzeit schreibgeschützt', + 'readonly.oneShot.body': 'Einmalige Aufgaben akzeptieren keine Folgeaufgaben; Sehen Sie sich hier den vollständigen Ausführungsbericht an.', + 'readonly.body': 'Die übergeordnete Sitzung ist offline. Öffnen Sie es erneut, um weiterhin Nachrichten zu senden.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m {seconds}s', + 'duration.days': '{days}d', + 'duration.daysHours': '{days}d {hours}h', + 'duration.months': '~{months}mo', + 'duration.monthsDays': '~{months}mo {days}d', + 'duration.years': '~{years}y', + 'duration.yearsMonths': '~{years}y {months}mo', + 'duration.exactDays': '{days}d {hours}h {minutes}m {seconds}s', + 'duration.exactTitle': 'Общая активная продолжительность: {duration}', + 'tokens.thousand': '{value}K', + 'tokens.million': '{value}M', + 'tokens.total': '{value} ток', + 'loading.label': 'Загрузка субагентов…', + 'load.error': 'Невозможно загрузить субагенты', + 'catalog.migrationRequired': 'Требуется миграция', + 'retry': 'Повторить', + 'mode.external': 'Внешняя задача', + 'mode.oneShot': 'одноразовый', + 'mode.continuable': 'продолжаемый', + 'mode.unknown': 'неизвестный режим', + 'readonly.unknown.body': 'Прочтите дочерний сеанс, чтобы определить, можно ли его продолжить.', + 'activity.running': 'бег', + 'activity.completed': 'завершено', + 'activity.inactive': 'не работает', + 'branch.collapse': 'Свернуть потомков {label}', + 'branch.expand': 'Развернуть потомков {label}', + 'count.total.one': 'Субагент {count}', + 'count.total.other': 'Субагенты {count}', + 'count.running.one': 'Субагент {count} работает', + 'count.running.other': 'Субагенты {count} работают', + 'switcher.aria': 'Переключить субагент: {title}', + 'tree.aria': 'Сеансы субагента', + 'open.sidebar': 'Открыть в боковой панели', + 'open.sidebar.aria': 'Откройте {label} на боковой панели.', + 'sidebar.chat': 'Чат', + 'readonly.oneShot.title': 'Разовая запись субагента', + 'readonly.title': 'Этот субагент пока доступен только для чтения.', + 'readonly.oneShot.body': 'Одноразовые задачи не допускают последующих действий; просмотреть полную запись выполнения можно здесь.', + 'readonly.body': 'Родительский сеанс находится в автономном режиме; откройте его повторно, чтобы продолжить отправку сообщений.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'duration.seconds': '{seconds}s', + 'duration.minutes': '{minutes}m {seconds}s', + 'duration.hours': '{hours}h {minutes}m {seconds}s', + 'duration.days': '{days}d', + 'duration.daysHours': '{days}d {hours}h', + 'duration.months': '~{months} mês', + 'duration.monthsDays': '~{months}mês {days}d', + 'duration.years': '~{years}y', + 'duration.yearsMonths': '~{years}a {months}mês', + 'duration.exactDays': '{days}d {hours}h {minutes}m {seconds}s', + 'duration.exactTitle': 'Duração total ativa: {duration}', + 'tokens.thousand': '{value}K', + 'tokens.million': '{value}M', + 'tokens.total': '{value} valor', + 'loading.label': 'Carregando subagentes…', + 'load.error': 'Não foi possível carregar subagentes', + 'catalog.migrationRequired': 'Migração necessária', + 'retry': 'Tentar novamente', + 'mode.external': 'Tarefa externa', + 'mode.oneShot': 'tiro único', + 'mode.continuable': 'continuável', + 'mode.unknown': 'modo desconhecido', + 'readonly.unknown.body': 'Leia a sessão secundária para determinar se ela pode continuar.', + 'activity.running': 'correndo', + 'activity.completed': 'concluído', + 'activity.inactive': 'não está funcionando', + 'branch.collapse': 'Recolher {label} descendentes', + 'branch.expand': 'Expandir {label} descendentes', + 'count.total.one': '{count} subagente', + 'count.total.other': '{count} subagentes', + 'count.running.one': '{count} subagente em execução', + 'count.running.other': '{count} subagentes em execução', + 'switcher.aria': 'Alternar subagente: {title}', + 'tree.aria': 'Sessões de subagente', + 'open.sidebar': 'Abrir na barra lateral', + 'open.sidebar.aria': 'Abra {label} na barra lateral', + 'sidebar.chat': 'Chat', + 'readonly.oneShot.title': 'Registro de subagente único', + 'readonly.title': 'Este subagente é somente leitura por enquanto', + 'readonly.oneShot.body': 'Tarefas únicas não aceitam acompanhamentos; revise o registro completo da execução aqui.', + 'readonly.body': 'A sessão pai está offline; reabra-o para continuar enviando mensagens.', +} satisfies Record diff --git a/packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx b/packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx index 7b1262cc11..56e34dc371 100644 --- a/packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx +++ b/packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx @@ -1099,6 +1099,8 @@ describe('SubagentHeaderLineage', () => { const current = screen.getByRole('treeitem', { name: /worker/ }) expect(current.getAttribute('aria-current')).toBe('true') expect(within(current).getByText('worker').className).toContain('currentLabel') + expect(within(current).queryByRole('button', { name: '在侧边栏打开 worker' })).toBeNull() + expect(within(screen.getByRole('treeitem', { name: /reviewer/ })).getByRole('button', { name: '在侧边栏打开 reviewer' })).toBeTruthy() fireEvent.click(screen.getByRole('treeitem', { name: /reviewer/ })) expect(input.openChild).toHaveBeenCalledWith({ parentSessionId: PARENT, diff --git a/packages/client/ui-theme/locale/de.json b/packages/client/ui-theme/locale/de.json new file mode 100644 index 0000000000..dc5c7ad008 --- /dev/null +++ b/packages/client/ui-theme/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstellenthema", + "description": "Bietet helle, dunkle und systembezogene Themeneinstellungen." + } +} diff --git a/packages/client/ui-theme/locale/es.json b/packages/client/ui-theme/locale/es.json new file mode 100644 index 0000000000..db2f55b5d8 --- /dev/null +++ b/packages/client/ui-theme/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Tema de interfaz", + "description": "Proporciona configuraciones de temas claros, oscuros y que siguen el sistema." + } +} diff --git a/packages/client/ui-theme/locale/fr.json b/packages/client/ui-theme/locale/fr.json new file mode 100644 index 0000000000..3ce1c36dc0 --- /dev/null +++ b/packages/client/ui-theme/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Thème de l'interface", + "description": "Fournit des paramètres de thème clair, sombre et suivant le système." + } +} diff --git a/packages/client/ui-theme/locale/pt.json b/packages/client/ui-theme/locale/pt.json new file mode 100644 index 0000000000..8cdd9e51cb --- /dev/null +++ b/packages/client/ui-theme/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Tema de interface", + "description": "Fornece configurações de tema claro, escuro e de acordo com o sistema." + } +} diff --git a/packages/client/ui-theme/locale/ru.json b/packages/client/ui-theme/locale/ru.json new file mode 100644 index 0000000000..de4dfcc5f3 --- /dev/null +++ b/packages/client/ui-theme/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Тема интерфейса", + "description": "Обеспечивает светлые, темные и системные настройки темы." + } +} diff --git a/packages/client/ui-theme/src/client/index.ts b/packages/client/ui-theme/src/client/index.ts index 0618c8a17e..a020fc35ae 100644 --- a/packages/client/ui-theme/src/client/index.ts +++ b/packages/client/ui-theme/src/client/index.ts @@ -27,7 +27,7 @@ import { createAppearanceRowStore, createFontRowStore } from './settings-store.t import { ICON_SYMBOLS, type IconSymbolId } from '@deepseek-ai/dsh-client-ui-primitives' import { installIconSprite, type IconReplacements } from './icon-sprite.ts' import { installThemeStyles } from './styles.ts' -import { en, ja, zh, type ThemeKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type ThemeKey } from './locales.ts' import { DEFAULT_FONT_SIZES, DEFAULT_PREFERENCE, FONT_FAMILY_FIELDS, FONT_ROLES, FONT_SIZE_SPECS, fontFamilyVariable, isFontSize, isThemePreference, normalizeFontFamily, sectionFontSizes, @@ -595,7 +595,7 @@ export const inject = ['slots', 'locale', 'remote', 'configForms'] */ export function apply(ctx: ClientContext): void { installThemeStyles(ctx) - ctx.effect(() => ctx.locale.register(SETTINGS_NS, { zh, en, ja }), 'ui-theme: settings row dictionaries') + ctx.effect(() => ctx.locale.register(SETTINGS_NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-theme: settings row dictionaries') const host = ctx.configForms.get(THEME_SETTINGS_NAMESPACE) const theme = new ThemeRuntime(ctx, host) ctx.provide('theme', theme) diff --git a/packages/client/ui-theme/src/client/locales.ts b/packages/client/ui-theme/src/client/locales.ts index b34cab3e2b..3e4888aa8c 100644 --- a/packages/client/ui-theme/src/client/locales.ts +++ b/packages/client/ui-theme/src/client/locales.ts @@ -89,3 +89,148 @@ export const ja = { 'fontFamily.placeholder': 'デフォルト', 'fontSize.more': 'その他のフォント設定', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'appearance.title': 'Apariencia', + 'appearance.light': 'Claro', + 'appearance.dark': 'Oscuro', + 'appearance.system': 'Sistema', + 'fontSize.text.title': 'Tamaño de fuente', + 'fontSize.text.description': 'Sólo afecta al contenido de la conversación.', + 'fontSize.text.increase': 'Aumentar el tamaño de fuente', + 'fontSize.text.decrease': 'Disminuir el tamaño de fuente', + 'fontSize.code.title': 'Tamaño de fuente del código', + 'fontSize.code.description': 'Se utiliza para bloques de código, código en línea y salida de herramientas.', + 'fontSize.code.increase': 'Aumentar el tamaño de fuente del código', + 'fontSize.code.decrease': 'Disminuir el tamaño de fuente del código', + 'fontSize.terminal.title': 'Tamaño de fuente del terminal', + 'fontSize.terminal.description': 'Utilizado para el terminal de la barra lateral.', + 'fontSize.terminal.increase': 'Aumentar el tamaño de fuente del terminal', + 'fontSize.terminal.decrease': 'Disminuir el tamaño de fuente del terminal', + 'fontSize.unit': 'píxeles', + 'fontFamily.text.title': 'Fuente de texto', + 'fontFamily.text.description': 'Se utiliza para la interfaz y el texto de la conversación. Separe las fuentes con comas; dejar vacío por defecto', + 'fontFamily.code.title': 'Fuente de código', + 'fontFamily.code.description': 'Se utiliza para bloques de código, código en línea y salida de herramientas. Separe las fuentes con comas; dejar vacío por defecto', + 'fontFamily.terminal.title': 'Fuente terminal', + 'fontFamily.terminal.description': 'Se utiliza para el terminal de la barra lateral. Separe las fuentes con comas; dejar vacío por defecto', + 'fontFamily.placeholder': 'Predeterminado', + 'fontSize.more': 'Más configuraciones de fuente', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'appearance.title': 'Apparence', + 'appearance.light': 'Clair', + 'appearance.dark': 'Sombre', + 'appearance.system': 'Système', + 'fontSize.text.title': 'Taille de la police', + 'fontSize.text.description': 'Affecte uniquement le contenu de la conversation', + 'fontSize.text.increase': 'Augmenter la taille de la police', + 'fontSize.text.decrease': 'Diminuer la taille de la police', + 'fontSize.code.title': 'Taille de la police du code', + 'fontSize.code.description': "Utilisé pour les blocs de code, le code en ligne et la sortie de l'outil", + 'fontSize.code.increase': 'Augmenter la taille de la police du code', + 'fontSize.code.decrease': 'Diminuer la taille de la police du code', + 'fontSize.terminal.title': 'Taille de la police du terminal', + 'fontSize.terminal.description': 'Utilisé pour le terminal de la barre latérale', + 'fontSize.terminal.increase': 'Augmenter la taille de la police du terminal', + 'fontSize.terminal.decrease': 'Diminuer la taille de la police du terminal', + 'fontSize.unit': 'px', + 'fontFamily.text.title': 'Police du texte', + 'fontFamily.text.description': "Utilisé pour l'interface et le texte de la conversation. Séparez les polices par des virgules ; laisser vide pour la valeur par défaut", + 'fontFamily.code.title': 'Police de code', + 'fontFamily.code.description': "Utilisé pour les blocs de code, le code en ligne et la sortie de l'outil. Séparez les polices par des virgules ; laisser vide pour la valeur par défaut", + 'fontFamily.terminal.title': 'Police du terminal', + 'fontFamily.terminal.description': 'Utilisé pour le terminal de la barre latérale. Séparez les polices par des virgules ; laisser vide pour la valeur par défaut', + 'fontFamily.placeholder': 'Par défaut', + 'fontSize.more': 'Plus de paramètres de police', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'appearance.title': 'Aussehen', + 'appearance.light': 'Hell', + 'appearance.dark': 'Dunkel', + 'appearance.system': 'System', + 'fontSize.text.title': 'Schriftgröße', + 'fontSize.text.description': 'Betrifft nur Konversationsinhalte', + 'fontSize.text.increase': 'Schriftgröße erhöhen', + 'fontSize.text.decrease': 'Schriftgröße verringern', + 'fontSize.code.title': 'Code-Schriftgröße', + 'fontSize.code.description': 'Wird für Codeblöcke, Inline-Code und Tool-Ausgabe verwendet', + 'fontSize.code.increase': 'Erhöhen Sie die Schriftgröße des Codes', + 'fontSize.code.decrease': 'Verringern Sie die Schriftgröße des Codes', + 'fontSize.terminal.title': 'Terminal-Schriftgröße', + 'fontSize.terminal.description': 'Wird für das Sidebar-Terminal verwendet', + 'fontSize.terminal.increase': 'Erhöhen Sie die Schriftgröße des Terminals', + 'fontSize.terminal.decrease': 'Verringern Sie die Schriftgröße des Terminals', + 'fontSize.unit': 'px', + 'fontFamily.text.title': 'Textschriftart', + 'fontFamily.text.description': 'Wird für die Benutzeroberfläche und den Konversationstext verwendet. Trennen Sie Schriftarten durch Kommas. Für die Standardeinstellung leer lassen', + 'fontFamily.code.title': 'Codeschriftart', + 'fontFamily.code.description': 'Wird für Codeblöcke, Inline-Code und Tool-Ausgabe verwendet. Trennen Sie Schriftarten durch Kommas. Für die Standardeinstellung leer lassen', + 'fontFamily.terminal.title': 'Terminal-Schriftart', + 'fontFamily.terminal.description': 'Wird für das Sidebar-Terminal verwendet. Trennen Sie Schriftarten durch Kommas. Für die Standardeinstellung leer lassen', + 'fontFamily.placeholder': 'Standard', + 'fontSize.more': 'Weitere Schriftarteinstellungen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'appearance.title': 'Внешний вид', + 'appearance.light': 'Светлая', + 'appearance.dark': 'Тёмная', + 'appearance.system': 'Система', + 'fontSize.text.title': 'Размер шрифта', + 'fontSize.text.description': 'Влияет только на содержание разговора', + 'fontSize.text.increase': 'Увеличить размер шрифта', + 'fontSize.text.decrease': 'Уменьшить размер шрифта', + 'fontSize.code.title': 'Размер шрифта кода', + 'fontSize.code.description': 'Используется для блоков кода, встроенного кода и выходных данных инструмента.', + 'fontSize.code.increase': 'Увеличить размер шрифта кода', + 'fontSize.code.decrease': 'Уменьшить размер шрифта кода', + 'fontSize.terminal.title': 'Размер шрифта терминала', + 'fontSize.terminal.description': 'Используется для терминала боковой панели', + 'fontSize.terminal.increase': 'Увеличьте размер шрифта терминала', + 'fontSize.terminal.decrease': 'Уменьшить размер шрифта терминала', + 'fontSize.unit': 'пикселей', + 'fontFamily.text.title': 'Текстовый шрифт', + 'fontFamily.text.description': 'Используется для интерфейса и текста разговора. Разделяйте шрифты запятыми; оставьте пустым по умолчанию', + 'fontFamily.code.title': 'Шрифт кода', + 'fontFamily.code.description': 'Используется для блоков кода, встроенного кода и выходных данных инструмента. Разделяйте шрифты запятыми; оставьте пустым по умолчанию', + 'fontFamily.terminal.title': 'Шрифт терминала', + 'fontFamily.terminal.description': 'Используется для терминала боковой панели. Разделяйте шрифты запятыми; оставьте пустым по умолчанию', + 'fontFamily.placeholder': 'По умолчанию', + 'fontSize.more': 'Дополнительные настройки шрифта', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'appearance.title': 'Aparência', + 'appearance.light': 'Claro', + 'appearance.dark': 'Escuro', + 'appearance.system': 'Sistema', + 'fontSize.text.title': 'Tamanho da fonte', + 'fontSize.text.description': 'Afeta apenas o conteúdo da conversa', + 'fontSize.text.increase': 'Aumentar o tamanho da fonte', + 'fontSize.text.decrease': 'Diminuir o tamanho da fonte', + 'fontSize.code.title': 'Tamanho da fonte do código', + 'fontSize.code.description': 'Usado para blocos de código, código embutido e saída de ferramenta', + 'fontSize.code.increase': 'Aumentar o tamanho da fonte do código', + 'fontSize.code.decrease': 'Diminuir o tamanho da fonte do código', + 'fontSize.terminal.title': 'Tamanho da fonte do terminal', + 'fontSize.terminal.description': 'Usado para o terminal da barra lateral', + 'fontSize.terminal.increase': 'Aumentar o tamanho da fonte do terminal', + 'fontSize.terminal.decrease': 'Diminuir o tamanho da fonte do terminal', + 'fontSize.unit': 'pixels', + 'fontFamily.text.title': 'Fonte do texto', + 'fontFamily.text.description': 'Usado para interface e texto de conversa. Separe as fontes com vírgulas; deixe em branco para o padrão', + 'fontFamily.code.title': 'Fonte do código', + 'fontFamily.code.description': 'Usado para blocos de código, código embutido e saída de ferramenta. Separe as fontes com vírgulas; deixe em branco para o padrão', + 'fontFamily.terminal.title': 'Fonte terminal', + 'fontFamily.terminal.description': 'Usado para o terminal da barra lateral. Separe as fontes com vírgulas; deixe em branco para o padrão', + 'fontFamily.placeholder': 'Padrão', + 'fontSize.more': 'Mais configurações de fonte', +} satisfies Record diff --git a/packages/client/ui-theme/tests/expected/radius-exceptions.expected.json b/packages/client/ui-theme/tests/expected/radius-exceptions.expected.json index 274fc5b552..aee0023246 100644 --- a/packages/client/ui-theme/tests/expected/radius-exceptions.expected.json +++ b/packages/client/ui-theme/tests/expected/radius-exceptions.expected.json @@ -29,7 +29,7 @@ ".leading: border-radius: 10px" ], "ui-schedule/src/client/TaskManagerPage.module.css": [ - ".newButton: border-radius: 16px", + ".headerAction: border-radius: 16px", ".filterTab: border-radius: 14px", ".searchField: border-radius: 12px", ".row: border-radius: 12px", diff --git a/packages/client/ui-tool/locale/de.json b/packages/client/ui-tool/locale/de.json new file mode 100644 index 0000000000..88a7b7e402 --- /dev/null +++ b/packages/client/ui-tool/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Anzeige des Werkzeugaufrufs", + "description": "Zeigt den Werkzeugaufrufprozess, verschachtelte Beziehungen und die Ergebnisschnittstelle jedes Werkzeugs an." + } +} diff --git a/packages/client/ui-tool/locale/es.json b/packages/client/ui-tool/locale/es.json new file mode 100644 index 0000000000..4798ae5cde --- /dev/null +++ b/packages/client/ui-tool/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Visualización de llamada de herramienta", + "description": "Muestra el proceso de llamada de herramientas, las relaciones anidadas y la interfaz de resultados de cada herramienta." + } +} diff --git a/packages/client/ui-tool/locale/fr.json b/packages/client/ui-tool/locale/fr.json new file mode 100644 index 0000000000..1728e08a11 --- /dev/null +++ b/packages/client/ui-tool/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Affichage des appels d'outils", + "description": "Affiche le processus d'appel de l'outil, les relations imbriquées et l'interface de résultat de chaque outil." + } +} diff --git a/packages/client/ui-tool/locale/pt.json b/packages/client/ui-tool/locale/pt.json new file mode 100644 index 0000000000..2500088c1b --- /dev/null +++ b/packages/client/ui-tool/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Exibição de chamada de ferramenta", + "description": "Mostra o processo de chamada da ferramenta, os relacionamentos aninhados e a interface de resultados de cada ferramenta." + } +} diff --git a/packages/client/ui-tool/locale/ru.json b/packages/client/ui-tool/locale/ru.json new file mode 100644 index 0000000000..dc1d297b73 --- /dev/null +++ b/packages/client/ui-tool/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Отображение вызова инструмента", + "description": "Показывает процесс вызова инструмента, вложенные связи и интерфейс результатов каждого инструмента." + } +} diff --git a/packages/client/ui-trajectory/locale/de.json b/packages/client/ui-trajectory/locale/de.json new file mode 100644 index 0000000000..a66a480012 --- /dev/null +++ b/packages/client/ui-trajectory/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Laufstrecke", + "description": "Sehen Sie sich laufende Aufzeichnungen wie Nachrichten, Werkzeugaufrufe, Zeitverbrauch und Token pro Runde an." + } +} diff --git a/packages/client/ui-trajectory/locale/es.json b/packages/client/ui-trajectory/locale/es.json new file mode 100644 index 0000000000..284dc23629 --- /dev/null +++ b/packages/client/ui-trajectory/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "pista de atletismo", + "description": "Vea registros en ejecución como mensajes, llamadas de herramientas, consumo de tiempo y tokens por ronda." + } +} diff --git a/packages/client/ui-trajectory/locale/fr.json b/packages/client/ui-trajectory/locale/fr.json new file mode 100644 index 0000000000..f61bd8a755 --- /dev/null +++ b/packages/client/ui-trajectory/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Piste de course", + "description": "Affichez les enregistrements en cours tels que les messages, les appels d'outils, la consommation de temps et les jetons par tour." + } +} diff --git a/packages/client/ui-trajectory/locale/pt.json b/packages/client/ui-trajectory/locale/pt.json new file mode 100644 index 0000000000..385830662e --- /dev/null +++ b/packages/client/ui-trajectory/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Pista de corrida", + "description": "Visualize registros em execução, como mensagens, chamadas de ferramentas, consumo de tempo e tokens por rodada." + } +} diff --git a/packages/client/ui-trajectory/locale/ru.json b/packages/client/ui-trajectory/locale/ru.json new file mode 100644 index 0000000000..049a1bae20 --- /dev/null +++ b/packages/client/ui-trajectory/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Беговая дорожка", + "description": "Просматривайте текущие записи, такие как сообщения, вызовы инструментов, затраты времени и токены, по раундам." + } +} diff --git a/packages/client/ui-trajectory/src/client/index.ts b/packages/client/ui-trajectory/src/client/index.ts index 05975721ad..5f33f9b806 100644 --- a/packages/client/ui-trajectory/src/client/index.ts +++ b/packages/client/ui-trajectory/src/client/index.ts @@ -16,7 +16,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type {} from '@deepseek-ai/dsh-client-ui-session/client' import { createTrajectoryDurationStore } from './duration-store.ts' import { createTrajectoryStringWrappingStore } from './string-wrapping-store.ts' -import { en, ja, NS, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, zh } from './locales.ts' import { registerTrajectoryAssistantDefinition } from './trajectory-assistant-definition.ts' import { registerTrajectoryCompactionDefinitions } from './trajectory-compaction-definition.ts' import { registerTrajectoryMessageDefinitions } from './trajectory-message-definitions.ts' @@ -59,7 +59,7 @@ export function apply(ctx: Context): void { } return source } - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-trajectory: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-trajectory: dictionaries') // Registration-time text (the view tab label) reads through the bound // translate as a thunk, so it follows the active locale without // re-registration. diff --git a/packages/client/ui-trajectory/src/client/locales.ts b/packages/client/ui-trajectory/src/client/locales.ts index 3dc996d556..b6c8e26929 100644 --- a/packages/client/ui-trajectory/src/client/locales.ts +++ b/packages/client/ui-trajectory/src/client/locales.ts @@ -604,3 +604,983 @@ export const ja = { 'layout.systemPromptAndToolsUpdated': 'システムプロンプトとツールを更新', 'layout.compactionInterrupted': '圧縮は完了前に中断されました。', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'view.trajectory': 'Trayectoria', + 'toolbar.aria': 'Barra de herramientas de trayectoria', + 'toolbar.duration': 'Duración', + 'toolbar.useActualDuration': 'Usar duración real', + 'toolbar.useEqualWidth': 'Utilice operaciones de igual ancho', + 'toolbar.actualTime': 'tiempo real', + 'toolbar.turns': 'Turnos', + 'toolbar.expandTurns': 'Expandir turnos', + 'toolbar.collapseTurns': 'Contraer turnos', + 'toolbar.calls': 'llamadas', + 'toolbar.expandCalls': 'Ampliar llamadas', + 'toolbar.collapseCalls': 'Contraer llamadas', + 'toolbar.search': 'Trayectoria de búsqueda', + 'toolbar.searchPlaceholder': 'Buscar', + 'kind.system': 'SISTEMA', + 'kind.user': 'USUARIO', + 'kind.context': 'CONTEXTO', + 'kind.compacted': 'COMPACTADO', + 'kind.message': 'Mensaje', + 'kind.assistant': 'ASISTENTE', + 'kind.tool': 'HERRAMIENTA', + 'kind.subtool': 'SUBHERRAMIENTA', + 'kind.sub': 'Subtítulo', + 'column.input': 'Entrada', + 'column.output': 'Salida', + 'column.think': 'pensar', + 'column.time': 'tiempo', + 'column.model': 'modelo', + 'column.tools': 'Herramientas', + 'turn.label': 'Turno {turn}', + 'section.betweenTurns': 'Entre turnos', + 'group.message': 'Mensaje', + 'group.step': 'Paso {step}', + 'group.compaction': 'Compactación {seq}', + 'status.failed': 'Fallido', + 'status.pending': 'Pendiente', + 'status.completed': 'Completado', + 'timing.notAvailable': 'No disponible', + 'timing.notRecorded': 'No registrado', + 'timing.stepStartUnavailable': 'Inicio de paso no disponible', + 'timing.firstTokenUnavailable': 'Primer token no disponible', + 'timing.usageUnavailable': 'Uso no disponible', + 'timing.outputTokensUnavailable': 'Tokens de salida no disponibles', + 'timing.durationTooShort': 'Duración demasiado corta', + 'timing.showLocalTime': 'Mostrar hora local', + 'timing.showUnixTimestamp': 'Mostrar marca de tiempo de Unix', + 'timing.started': 'iniciado', + 'timing.totalDuration': 'Duración total', + 'timing.ttft': 'TTFT', + 'timing.generation': 'Generación', + 'timing.throughput': 'Rendimiento', + 'timing.duration': 'Duración', + 'timing.source': 'fuente de tiempo', + 'timing.sessionTimestamps': 'Marcas de tiempo de sesión', + 'timing.sessionTimestampsRunning': 'Marcas de tiempo de sesión (en ejecución)', + 'timing.request': 'Solicitar tiempo', + 'unit.milliseconds': '{value} ms', + 'unit.seconds': '{value} s', + 'unit.tokens': 'Tok {value}', + 'unit.tokensPerSecond': '{value} tok/s', + 'usage.tokens': 'Fichas', + 'usage.reasoning': 'Razonamiento', + 'usage.content': 'Contenido', + 'usage.notReported': 'Uso no reportado', + 'usage.input': 'Entrada', + 'usage.cached': 'En caché', + 'usage.cacheCreated': 'Caché creada', + 'usage.other': 'Otro', + 'usage.output': 'Salida', + 'usage.thisRequest': 'Esta solicitud', + 'usage.sessionCumulative': 'Sesión acumulativa', + 'options.notRecorded': 'Opciones no registradas', + 'options.json': 'Solicitar opciones JSON', + 'source.unknown': 'Desconocido', + 'source.user': 'Usuario', + 'source.plugin': 'Complemento', + 'source.pluginNamed': 'Complemento · {plugin}', + 'source.goal': 'Objetivo', + 'source.goalRound': 'Gol · Ronda {round}', + 'source.notRecorded': 'Fuente no registrada', + 'source.messageJson': 'Fuente del mensaje JSON', + 'tab.summary': 'Resumen', + 'tab.rawOutput': 'Salida bruta', + 'tab.preview': 'Vista previa', + 'tab.raw': 'crudo', + 'tab.source': 'Fuente', + 'tab.payload': 'Carga útil', + 'tab.result': 'Resultado', + 'tab.schema': 'esquema', + 'tab.timing': 'Sincronización', + 'tab.diff': 'diferencia', + 'tab.systemPrompt': 'Aviso del sistema', + 'tab.tools': 'Herramientas', + 'tab.options': 'Opciones', + 'tab.usage': 'Uso', + 'record.toolCallOnly': '(solo llamada de herramienta)', + 'record.noContent': 'Sin contenido', + 'record.noPayload': 'Ninguna carga útil capturada', + 'record.noResult': 'Ningún resultado capturado', + 'record.noOutput': 'Sin salida', + 'record.schemaUnavailable': 'Esquema no disponible', + 'record.parameters': 'Parámetros', + 'record.resultJson': 'Resultado JSON', + 'record.json': 'JSON', + 'record.parametersJson': 'parámetros JSON', + 'record.namedParametersJson': 'Parámetros de {name} JSON', + 'record.payloadJson': 'Carga útil JSON', + 'record.outputJson': 'Resultado JSON', + 'record.thinking': 'Pensando', + 'record.wrapLines': 'Ajustar líneas', + 'code.source': 'Código', + 'code.output': 'Salida', + 'code.copySource': 'Copiar código', + 'code.copyOutput': 'Copiar salida', + 'code.originalJson': 'Originales JSON.', + 'code.running': 'Corriendo…', + 'record.systemPromptMissing': 'No hay mensaje del sistema en esta solicitud', + 'record.toolsMissing': 'No hay herramientas en esta solicitud', + 'record.systemPrompt': 'Aviso del sistema', + 'record.tools': 'Herramientas', + 'block.openSummary': 'Resumen de llamadas a la herramienta Open Block #{index}', + 'block.openSummaryTitle': 'Resumen de llamada de herramienta abierta', + 'block.label': 'Bloque #{index} {type}', + 'history.loadingTrajectory': 'Cargando trayectoria…', + 'history.loadingEarlier': 'Cargando historial anterior…', + 'history.loadingEarlierAria': 'Cargando historial anterior…', + 'history.loadEarlier': 'Cargar historial anterior', + 'history.clickToLoadEarlier': 'Haga clic para cargar el historial anterior', + 'request.label': 'Solicitud #{request}', + 'request.labelCompaction': 'Solicitud #{request} · Compactación', + 'request.compaction': 'Compactación · {section}', + 'request.compactionPurpose': 'Compactación', + 'request.retryProgress': '{retry} de {maximum}', + 'request.collapsedSummary': 'Resumen de {kind} contraído, {summary}', + 'request.collapsedTurn': 'turno', + 'request.collapsedAssistant': 'asistente', + 'request.rowAria': '{request}{kind}, {content}', + 'request.rowPrefix': 'Solicitar {request}, ', + 'request.rowAriaCompaction': 'Solicitar {request}, compactación', + 'request.noContent': 'sin contenido', + 'summary.toolCalls.one': 'Llamada a la herramienta {count}', + 'summary.toolCalls.other': 'Llamadas a herramientas {count}', + 'summary.steps.one': 'Paso {count}', + 'summary.steps.other': 'Pasos {count}', + 'details.event': 'Detalles del evento', + 'details.resize': 'Cambiar el tamaño de los detalles del evento', + 'details.resizeTitle': 'Arrastre para cambiar el tamaño. Haga doble clic para restablecer.', + 'details.close': 'Cerrar detalles', + 'details.status': 'Estado', + 'details.purpose': 'Propósito', + 'details.provider': 'Proveedor', + 'details.model': 'modelo', + 'details.toolCalls': 'Llamadas a herramientas', + 'details.subtoolCalls': 'Llamadas a subherramientas', + 'details.error': 'error', + 'details.lastAttemptError': 'Error del último intento', + 'details.failure.auth': 'La clave API no es válida', + 'details.retry': 'Reintentar', + 'details.retryDelay': 'Retraso de reintento', + 'details.result': 'Resultado', + 'details.compacted': 'compactado', + 'details.assistantMessage': 'Mensaje del asistente', + 'details.source': 'Fuente', + 'details.hierarchy': 'Jerarquía', + 'details.toolCall': 'Llamada de herramienta', + 'timeline.aria': 'Cronología de la trayectoria', + 'timeline.overviewAria': 'Descripción general de la línea de tiempo; arrastre horizontalmente para enfocar eventos', + 'timeline.noTimingData': 'Sin datos de tiempo', + 'timeline.total': 'Total {duration}', + 'timeline.started': 'Iniciado {time}', + 'timeline.ttftDecoding': 'TTFT {ttft} · Decodificación {decoding}', + 'layout.compacting': 'Contexto de compactación…', + 'layout.compactionFailed': 'Falló la compactación', + 'layout.compacted': 'Contexto compactado', + 'layout.toolCallOnly': 'Sólo llamada de herramienta', + 'attachment.list': 'Adjuntos', + 'attachment.imageName': 'Imagen {index}', + 'layout.imageCount': 'Imágenes ×{count}', + 'layout.fileAttachments': 'Archivos ×{count}', + 'layout.initialSystemPrompt': 'Aviso inicial del sistema', + 'layout.systemPromptUpdated': 'Aviso del sistema actualizado', + 'layout.toolsUpdated': 'Herramientas actualizadas', + 'layout.toolAdded': 'Herramienta agregada: {name}', + 'layout.toolRemoved': 'Herramienta eliminada: {name}', + 'layout.toolUpdateNotice': 'Herramientas actualizadas', + 'layout.toolsAdded': 'Añadido: {names}', + 'layout.toolsAddedCount': '{count} añadido', + 'layout.toolsChanged': '{added} agregado, {removed} eliminado', + 'layout.toolsRemoved': 'Eliminado: {names}', + 'layout.toolsRemovedCount': '{count} eliminado', + 'layout.systemPromptAndToolsUpdated': 'Aviso del sistema y herramientas actualizadas', + 'layout.compactionInterrupted': 'La compactación se interrumpió antes de finalizar.', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'view.trajectory': 'Trajectoire', + 'toolbar.aria': 'Barre d’outils du parcours', + 'toolbar.duration': 'Durée', + 'toolbar.useActualDuration': 'Utiliser la durée réelle', + 'toolbar.useEqualWidth': 'Utiliser des opérations de largeur égale', + 'toolbar.actualTime': 'Temps réel', + 'toolbar.turns': 'Tours', + 'toolbar.expandTurns': 'Développer les tours', + 'toolbar.collapseTurns': 'Réduire les tours', + 'toolbar.calls': 'Appels', + 'toolbar.expandCalls': 'Développer les appels', + 'toolbar.collapseCalls': 'Réduire les appels', + 'toolbar.search': 'Rechercher dans le parcours', + 'toolbar.searchPlaceholder': 'Rechercher', + 'kind.system': 'SYSTÈME', + 'kind.user': 'UTILISATEUR', + 'kind.context': 'CONTEXTE', + 'kind.compacted': 'COMPACTÉ', + 'kind.message': 'Message', + 'kind.assistant': 'ASSISTANT', + 'kind.tool': 'OUTIL', + 'kind.subtool': 'SOUS-OUTIL', + 'kind.sub': 'Sous', + 'column.input': 'Entrée', + 'column.output': 'Sortie', + 'column.think': 'Pensez', + 'column.time': 'Temps', + 'column.model': 'Modèle', + 'column.tools': 'Outils', + 'turn.label': 'Tour {turn}', + 'section.betweenTurns': 'Entre les tours', + 'group.message': 'Message', + 'group.step': 'Étape {step}', + 'group.compaction': 'Compactage {seq}', + 'status.failed': 'Échec', + 'status.pending': 'En attente', + 'status.completed': 'Terminé', + 'timing.notAvailable': 'Non disponible', + 'timing.notRecorded': 'Non enregistré', + 'timing.stepStartUnavailable': 'Démarrage par étapes indisponible', + 'timing.firstTokenUnavailable': 'Premier jeton indisponible', + 'timing.usageUnavailable': 'Utilisation indisponible', + 'timing.outputTokensUnavailable': 'Jetons de sortie indisponibles', + 'timing.durationTooShort': 'Durée trop courte', + 'timing.showLocalTime': "Afficher l'heure locale", + 'timing.showUnixTimestamp': "Afficher l'horodatage Unix", + 'timing.started': 'Commencé', + 'timing.totalDuration': 'Durée totale', + 'timing.ttft': 'TTFT', + 'timing.generation': 'Génération', + 'timing.throughput': 'Débit', + 'timing.duration': 'Durée', + 'timing.source': 'Source de synchronisation', + 'timing.sessionTimestamps': 'Horodatage des sessions', + 'timing.sessionTimestampsRunning': "Horodatages de session (en cours d'exécution)", + 'timing.request': 'Calendrier de la demande', + 'unit.milliseconds': '{value} ms', + 'unit.seconds': '{value}s', + 'unit.tokens': 'Jeton {value}', + 'unit.tokensPerSecond': '{value} jetons/s', + 'usage.tokens': 'Jetons', + 'usage.reasoning': 'Raisonnement', + 'usage.content': 'Contenu', + 'usage.notReported': 'Utilisation non signalée', + 'usage.input': 'Entrée', + 'usage.cached': 'En cache', + 'usage.cacheCreated': 'Cache créé', + 'usage.other': 'Autre', + 'usage.output': 'Sortie', + 'usage.thisRequest': 'Cette demande', + 'usage.sessionCumulative': 'Session cumulée', + 'options.notRecorded': 'Options non enregistrées', + 'options.json': 'Options de demande JSON', + 'source.unknown': 'Inconnu', + 'source.user': 'Utilisateur', + 'source.plugin': 'Plugin', + 'source.pluginNamed': 'Plugin · {plugin}', + 'source.goal': 'Objectif', + 'source.goalRound': 'Objectif · Tour {round}', + 'source.notRecorded': 'Source non enregistrée', + 'source.messageJson': 'Origine du message JSON', + 'tab.summary': 'Résumé', + 'tab.rawOutput': 'Sortie brute', + 'tab.preview': 'Aperçu', + 'tab.raw': 'Brut', + 'tab.source': 'Source', + 'tab.payload': 'Charge utile', + 'tab.result': 'Résultat', + 'tab.schema': 'Schéma', + 'tab.timing': 'Calendrier', + 'tab.diff': 'Différence', + 'tab.systemPrompt': 'Invite système', + 'tab.tools': 'Outils', + 'tab.options': 'Possibilités', + 'tab.usage': 'Utilisation', + 'record.toolCallOnly': "(appel d'outil uniquement)", + 'record.noContent': 'Aucun contenu', + 'record.noPayload': 'Aucune charge utile capturée', + 'record.noResult': 'Aucun résultat capturé', + 'record.noOutput': 'Aucune sortie', + 'record.schemaUnavailable': 'Schéma indisponible', + 'record.parameters': 'Paramètres', + 'record.resultJson': 'Résultat JSON', + 'record.json': 'JSON', + 'record.parametersJson': 'paramètres JSON', + 'record.namedParametersJson': 'Paramètres {name} JSON', + 'record.payloadJson': 'Charge utile JSON', + 'record.outputJson': 'Résultat JSON', + 'record.thinking': 'Réflexion', + 'record.wrapLines': 'Activer le retour à la ligne', + 'code.source': 'Coder', + 'code.output': 'Sortie', + 'code.copySource': 'Copier le code', + 'code.copyOutput': 'Copier la sortie', + 'code.originalJson': 'JSON d’origine', + 'code.running': 'Courir…', + 'record.systemPromptMissing': 'Aucune invite système dans cette requête', + 'record.toolsMissing': 'Aucun outil dans cette demande', + 'record.systemPrompt': 'Invite système', + 'record.tools': 'Outils', + 'block.openSummary': "Résumé de l'appel de l'outil Open Block #{index}", + 'block.openSummaryTitle': "Résumé de l'appel d'outil ouvert", + 'block.label': 'Bloc #{index} {type}', + 'history.loadingTrajectory': 'Chargement de la trajectoire…', + 'history.loadingEarlier': "Chargement de l'historique antérieur…", + 'history.loadingEarlierAria': "Chargement de l'historique antérieur…", + 'history.loadEarlier': "Charger l'historique antérieur", + 'history.clickToLoadEarlier': "Cliquez pour charger l'historique précédent", + 'request.label': 'Demande #{request}', + 'request.labelCompaction': 'Demande #{request} · Compactage', + 'request.compaction': 'Compactage · {section}', + 'request.compactionPurpose': 'Compactage', + 'request.retryProgress': '{retry} ou {maximum}', + 'request.collapsedSummary': 'Résumé {kind} réduit, {summary}', + 'request.collapsedTurn': 'tour', + 'request.collapsedAssistant': 'assistant', + 'request.rowAria': '{request}{kind}, {content}', + 'request.rowPrefix': 'Demande {request}, ', + 'request.rowAriaCompaction': 'Demande {request}, compactage', + 'request.noContent': 'pas de contenu', + 'summary.toolCalls.one': "Appel d'outil {count}", + 'summary.toolCalls.other': "Appels d'outil {count}", + 'summary.steps.one': 'Étape {count}', + 'summary.steps.other': 'Étapes {count}', + 'details.event': "Détails de l'événement", + 'details.resize': "Redimensionner les détails de l'événement", + 'details.resizeTitle': 'Faites glisser pour redimensionner. Double-cliquez pour réinitialiser.', + 'details.close': 'Fermer les détails', + 'details.status': 'Statut', + 'details.purpose': 'Objectif', + 'details.provider': 'Fournisseur', + 'details.model': 'Modèle', + 'details.toolCalls': "Appels d'outils", + 'details.subtoolCalls': 'Appels de sous-outils', + 'details.error': 'Erreur', + 'details.lastAttemptError': 'Erreur de dernière tentative', + 'details.failure.auth': "La clé API n'est pas valide", + 'details.retry': 'Réessayer', + 'details.retryDelay': 'Délai de nouvelle tentative', + 'details.result': 'Résultat', + 'details.compacted': 'Compacté', + 'details.assistantMessage': "Message de l'assistant", + 'details.source': 'Source', + 'details.hierarchy': 'Hiérarchie', + 'details.toolCall': "Appel d'outil", + 'timeline.aria': 'Chronologie de la trajectoire', + 'timeline.overviewAria': 'Aperçu de la chronologie ; faites glisser horizontalement pour concentrer les événements', + 'timeline.noTimingData': 'Aucune donnée de synchronisation', + 'timeline.total': 'Total {duration}', + 'timeline.started': 'Démarré {time}', + 'timeline.ttftDecoding': 'TTFT {ttft} · Décodage {decoding}', + 'layout.compacting': 'Contexte de compactage…', + 'layout.compactionFailed': 'Échec du compactage', + 'layout.compacted': 'Contexte compacté', + 'layout.toolCallOnly': "Appel d'outil uniquement", + 'attachment.list': 'Pièces jointes', + 'attachment.imageName': 'Image {index}', + 'layout.imageCount': 'Images ×{count}', + 'layout.fileAttachments': 'Fichiers ×{count}', + 'layout.initialSystemPrompt': 'Invite système initiale', + 'layout.systemPromptUpdated': 'Invite système mise à jour', + 'layout.toolsUpdated': 'Outils mis à jour', + 'layout.toolAdded': 'Outil ajouté : {name}', + 'layout.toolRemoved': 'Outil supprimé : {name}', + 'layout.toolUpdateNotice': 'Outils mis à jour', + 'layout.toolsAdded': 'Ajouté : {names}', + 'layout.toolsAddedCount': '{count} ajouté', + 'layout.toolsChanged': '{added} ajouté, {removed} supprimé', + 'layout.toolsRemoved': 'Supprimé : {names}', + 'layout.toolsRemovedCount': '{count} supprimé', + 'layout.systemPromptAndToolsUpdated': 'Invite système et outils mis à jour', + 'layout.compactionInterrupted': 'Le compactage a été interrompu avant la fin.', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'view.trajectory': 'Ausführungsverlauf', + 'toolbar.aria': 'Symbolleiste „Ausführungsverlauf“.', + 'toolbar.duration': 'Dauer', + 'toolbar.useActualDuration': 'Verwenden Sie die tatsächliche Dauer', + 'toolbar.useEqualWidth': 'Verwenden Sie Operationen gleicher Breite', + 'toolbar.actualTime': 'Tatsächliche Zeit', + 'toolbar.turns': 'Runden', + 'toolbar.expandTurns': 'Runden ausklappen', + 'toolbar.collapseTurns': 'Runden einklappen', + 'toolbar.calls': 'Aufrufe', + 'toolbar.expandCalls': 'Aufrufe erweitern', + 'toolbar.collapseCalls': 'Aufrufe reduzieren', + 'toolbar.search': 'Suchbahn', + 'toolbar.searchPlaceholder': 'Suchen', + 'kind.system': 'SYSTEM', + 'kind.user': 'BENUTZER', + 'kind.context': 'KONTEXT', + 'kind.compacted': 'KOMPRIMIERT', + 'kind.message': 'Nachricht', + 'kind.assistant': 'ASSISTENT', + 'kind.tool': 'WERKZEUG', + 'kind.subtool': 'UNTERWERKZEUG', + 'kind.sub': 'Sub', + 'column.input': 'Eingabe', + 'column.output': 'Ausgabe', + 'column.think': 'Denken Sie nach', + 'column.time': 'Zeit', + 'column.model': 'Modell', + 'column.tools': 'Werkzeuge', + 'turn.label': 'Runde {turn}', + 'section.betweenTurns': 'Zwischen den Runden', + 'group.message': 'Nachricht', + 'group.step': 'Schritt {step}', + 'group.compaction': 'Verdichtung {seq}', + 'status.failed': 'Fehlgeschlagen', + 'status.pending': 'Ausstehend', + 'status.completed': 'Abgeschlossen', + 'timing.notAvailable': 'Nicht verfügbar', + 'timing.notRecorded': 'Nicht aufgezeichnet', + 'timing.stepStartUnavailable': 'Schrittstart nicht verfügbar', + 'timing.firstTokenUnavailable': 'Erstes Token nicht verfügbar', + 'timing.usageUnavailable': 'Nutzung nicht verfügbar', + 'timing.outputTokensUnavailable': 'Ausgabetoken nicht verfügbar', + 'timing.durationTooShort': 'Dauer zu kurz', + 'timing.showLocalTime': 'Ortszeit anzeigen', + 'timing.showUnixTimestamp': 'Unix-Zeitstempel anzeigen', + 'timing.started': 'Begonnen', + 'timing.totalDuration': 'Gesamtdauer', + 'timing.ttft': 'TTFT', + 'timing.generation': 'Generation', + 'timing.throughput': 'Durchsatz', + 'timing.duration': 'Dauer', + 'timing.source': 'Timing-Quelle', + 'timing.sessionTimestamps': 'Sitzungszeitstempel', + 'timing.sessionTimestampsRunning': 'Sitzungszeitstempel (laufend)', + 'timing.request': 'Anfragezeitpunkt', + 'unit.milliseconds': '{value} ms', + 'unit.seconds': '{value} s', + 'unit.tokens': '{value}-Token', + 'unit.tokensPerSecond': '{value} tok/s', + 'usage.tokens': 'Token', + 'usage.reasoning': 'Begründung', + 'usage.content': 'Inhalt', + 'usage.notReported': 'Nutzung nicht gemeldet', + 'usage.input': 'Eingabe', + 'usage.cached': 'Zwischengespeichert', + 'usage.cacheCreated': 'Cache erstellt', + 'usage.other': 'Andere', + 'usage.output': 'Ausgabe', + 'usage.thisRequest': 'Diese Anfrage', + 'usage.sessionCumulative': 'Sitzung kumulativ', + 'options.notRecorded': 'Optionen nicht aufgezeichnet', + 'options.json': 'Anfrageoptionen JSON', + 'source.unknown': 'Unbekannt', + 'source.user': 'Benutzer', + 'source.plugin': 'Plugin', + 'source.pluginNamed': 'Plugin · {plugin}', + 'source.goal': 'Ziel', + 'source.goalRound': 'Ziel · Runde {round}', + 'source.notRecorded': 'Quelle nicht erfasst', + 'source.messageJson': 'Nachrichtenquelle JSON', + 'tab.summary': 'Zusammenfassung', + 'tab.rawOutput': 'Rohausgabe', + 'tab.preview': 'Vorschau', + 'tab.raw': 'Roh', + 'tab.source': 'Quelle', + 'tab.payload': 'Nutzlast', + 'tab.result': 'Ergebnis', + 'tab.schema': 'Schema', + 'tab.timing': 'Timing', + 'tab.diff': 'Diff', + 'tab.systemPrompt': 'Systemaufforderung', + 'tab.tools': 'Werkzeuge', + 'tab.options': 'Optionen', + 'tab.usage': 'Nutzung', + 'record.toolCallOnly': '(nur Werkzeugaufruf)', + 'record.noContent': 'Kein Inhalt', + 'record.noPayload': 'Keine Nutzlast erfasst', + 'record.noResult': 'Kein Ergebnis erfasst', + 'record.noOutput': 'Keine Ausgabe', + 'record.schemaUnavailable': 'Schema nicht verfügbar', + 'record.parameters': 'Parameter', + 'record.resultJson': 'Ergebnis JSON', + 'record.json': 'JSON', + 'record.parametersJson': 'Parameter JSON', + 'record.namedParametersJson': '{name} Parameter JSON', + 'record.payloadJson': 'Nutzlast JSON', + 'record.outputJson': 'Ergebnis JSON', + 'record.thinking': 'Nachdenken', + 'record.wrapLines': 'Zeilen umbrechen', + 'code.source': 'Code', + 'code.output': 'Ausgabe', + 'code.copySource': 'Code kopieren', + 'code.copyOutput': 'Ausgabe kopieren', + 'code.originalJson': 'Original JSON', + 'code.running': 'Laufen…', + 'record.systemPromptMissing': 'In dieser Anfrage gibt es keine Systemaufforderung', + 'record.toolsMissing': 'In dieser Anfrage sind keine Werkzeuge enthalten', + 'record.systemPrompt': 'Systemaufforderung', + 'record.tools': 'Werkzeuge', + 'block.openSummary': 'Zusammenfassung des Toolaufrufs „Block #{index} öffnen“.', + 'block.openSummaryTitle': 'Zusammenfassung des Tool-Aufrufs öffnen', + 'block.label': 'Block #{index} {type}', + 'history.loadingTrajectory': 'Ausführungsverlauf wird geladen…', + 'history.loadingEarlier': 'Früherer Verlauf wird geladen…', + 'history.loadingEarlierAria': 'Früherer Verlauf wird geladen…', + 'history.loadEarlier': 'Früheren Verlauf laden', + 'history.clickToLoadEarlier': 'Klicken Sie hier, um den früheren Verlauf zu laden', + 'request.label': 'Fordern Sie #{request} an', + 'request.labelCompaction': 'Anfrage #{request} · Komprimierung', + 'request.compaction': 'Verdichtung · {section}', + 'request.compactionPurpose': 'Verdichtung', + 'request.retryProgress': '{retry} von {maximum}', + 'request.collapsedSummary': 'Reduzierte {kind}-Zusammenfassung, {summary}', + 'request.collapsedTurn': 'Runde', + 'request.collapsedAssistant': 'Assistent', + 'request.rowAria': '{request}{kind}, {content}', + 'request.rowPrefix': 'Anfrage {request}, ', + 'request.rowAriaCompaction': 'Anfrage {request}, Verdichtung', + 'request.noContent': 'kein Inhalt', + 'summary.toolCalls.one': 'Werkzeugaufruf {count}', + 'summary.toolCalls.other': '{count} Toolaufrufe', + 'summary.steps.one': '{count} Schritt', + 'summary.steps.other': '{count} Schritte', + 'details.event': 'Veranstaltungsdetails', + 'details.resize': 'Ändern Sie die Größe der Veranstaltungsdetails', + 'details.resizeTitle': 'Ziehen Sie, um die Größe zu ändern. Zum Zurücksetzen doppelklicken.', + 'details.close': 'Details schließen', + 'details.status': 'Status', + 'details.purpose': 'Zweck', + 'details.provider': 'Anbieter', + 'details.model': 'Modell', + 'details.toolCalls': 'Werkzeugaufrufe', + 'details.subtoolCalls': 'Subtool-Aufrufe', + 'details.error': 'Fehler', + 'details.lastAttemptError': 'Fehler beim letzten Versuch', + 'details.failure.auth': 'Der API-Schlüssel ist ungültig', + 'details.retry': 'Erneut versuchen', + 'details.retryDelay': 'Wiederholungsverzögerung', + 'details.result': 'Ergebnis', + 'details.compacted': 'Verdichtet', + 'details.assistantMessage': 'Assistentennachricht', + 'details.source': 'Quelle', + 'details.hierarchy': 'Hierarchie', + 'details.toolCall': 'Werkzeugaufruf', + 'timeline.aria': 'Zeitleiste der Ausführungsverlauf', + 'timeline.overviewAria': 'Zeitleistenübersicht; Ziehen Sie horizontal, um Ereignisse zu fokussieren', + 'timeline.noTimingData': 'Keine Zeitdaten', + 'timeline.total': 'Insgesamt {duration}', + 'timeline.started': '{time} gestartet', + 'timeline.ttftDecoding': 'TTFT {ttft} · Dekodierung {decoding}', + 'layout.compacting': 'Kontext verdichten…', + 'layout.compactionFailed': 'Die Komprimierung ist fehlgeschlagen', + 'layout.compacted': 'Kontext verdichtet', + 'layout.toolCallOnly': 'Nur Werkzeugaufruf', + 'attachment.list': 'Anhänge', + 'attachment.imageName': 'Bild {index}', + 'layout.imageCount': 'Bilder ×{count}', + 'layout.fileAttachments': 'Dateien ×{count}', + 'layout.initialSystemPrompt': 'Erste Systemaufforderung', + 'layout.systemPromptUpdated': 'Systemaufforderung aktualisiert', + 'layout.toolsUpdated': 'Werkzeuge aktualisiert', + 'layout.toolAdded': 'Tool hinzugefügt: {name}', + 'layout.toolRemoved': 'Entferntes Werkzeug: {name}', + 'layout.toolUpdateNotice': 'Werkzeuge aktualisiert', + 'layout.toolsAdded': 'Hinzugefügt: {names}', + 'layout.toolsAddedCount': '{count} hinzugefügt', + 'layout.toolsChanged': '{added} hinzugefügt, {removed} entfernt', + 'layout.toolsRemoved': 'Entfernt: {names}', + 'layout.toolsRemovedCount': '{count} entfernt', + 'layout.systemPromptAndToolsUpdated': 'Systemaufforderung und Tools aktualisiert', + 'layout.compactionInterrupted': 'Die Verdichtung wurde vor der Fertigstellung unterbrochen.', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'view.trajectory': 'Траектория', + 'toolbar.aria': 'Панель инструментов траектории', + 'toolbar.duration': 'Продолжительность', + 'toolbar.useActualDuration': 'Использовать фактическую продолжительность', + 'toolbar.useEqualWidth': 'Используйте операции равной ширины', + 'toolbar.actualTime': 'Фактическое время', + 'toolbar.turns': 'Ходы', + 'toolbar.expandTurns': 'Развернуть ходы', + 'toolbar.collapseTurns': 'Свернуть ходы', + 'toolbar.calls': 'Звонки', + 'toolbar.expandCalls': 'Развернуть вызовы', + 'toolbar.collapseCalls': 'Свернуть вызовы', + 'toolbar.search': 'Траектория поиска', + 'toolbar.searchPlaceholder': 'Поиск', + 'kind.system': 'СИСТЕМА', + 'kind.user': 'ПОЛЬЗОВАТЕЛЬ', + 'kind.context': 'КОНТЕКСТ', + 'kind.compacted': 'СЖАТО', + 'kind.message': 'Сообщение', + 'kind.assistant': 'АССИСТЕНТ', + 'kind.tool': 'ИНСТРУМЕНТ', + 'kind.subtool': 'ВЛОЖЕННЫЙ ИНСТРУМЕНТ', + 'kind.sub': 'Саб', + 'column.input': 'Ввод', + 'column.output': 'Выход', + 'column.think': 'Подумайте', + 'column.time': 'Время', + 'column.model': 'Модель', + 'column.tools': 'Инструменты', + 'turn.label': 'Ход {turn}', + 'section.betweenTurns': 'Между ходами', + 'group.message': 'Сообщение', + 'group.step': 'Шаг {step}', + 'group.compaction': 'Уплотнение {seq}', + 'status.failed': 'Не удалось', + 'status.pending': 'Ожидается', + 'status.completed': 'Завершено', + 'timing.notAvailable': 'Недоступно', + 'timing.notRecorded': 'Не записано', + 'timing.stepStartUnavailable': 'Начало шага недоступно', + 'timing.firstTokenUnavailable': 'Первый токен недоступен', + 'timing.usageUnavailable': 'Использование недоступно', + 'timing.outputTokensUnavailable': 'Выходные токены недоступны.', + 'timing.durationTooShort': 'Продолжительность слишком коротка', + 'timing.showLocalTime': 'Показать местное время', + 'timing.showUnixTimestamp': 'Показать временную метку Unix', + 'timing.started': 'Началось', + 'timing.totalDuration': 'Общая продолжительность', + 'timing.ttft': 'TTFT', + 'timing.generation': 'Поколение', + 'timing.throughput': 'Пропускная способность', + 'timing.duration': 'Продолжительность', + 'timing.source': 'Источник синхронизации', + 'timing.sessionTimestamps': 'Временные метки сеанса', + 'timing.sessionTimestampsRunning': 'Временные метки сеанса (работает)', + 'timing.request': 'Запрос времени', + 'unit.milliseconds': '{value} мс', + 'unit.seconds': '{value} с', + 'unit.tokens': '{value} ток', + 'unit.tokensPerSecond': '{value} ток/с', + 'usage.tokens': 'Токены', + 'usage.reasoning': 'Рассуждение', + 'usage.content': 'Содержание', + 'usage.notReported': 'Об использовании не сообщается', + 'usage.input': 'Ввод', + 'usage.cached': 'Кэшированный', + 'usage.cacheCreated': 'Кэш создан', + 'usage.other': 'Другое', + 'usage.output': 'Выход', + 'usage.thisRequest': 'Этот запрос', + 'usage.sessionCumulative': 'Совокупный сеанс', + 'options.notRecorded': 'Опции не записаны', + 'options.json': 'Варианты запроса JSON', + 'source.unknown': 'Неизвестно', + 'source.user': 'Пользователь', + 'source.plugin': 'Плагин', + 'source.pluginNamed': 'Плагин · {plugin}', + 'source.goal': 'Цель', + 'source.goalRound': 'Гол · Раунд {round}', + 'source.notRecorded': 'Источник не записан', + 'source.messageJson': 'Источник сообщения JSON', + 'tab.summary': 'Резюме', + 'tab.rawOutput': 'Необработанный вывод', + 'tab.preview': 'Предварительный просмотр', + 'tab.raw': 'сырой', + 'tab.source': 'Источник', + 'tab.payload': 'Полезная нагрузка', + 'tab.result': 'Результат', + 'tab.schema': 'Схема', + 'tab.timing': 'Тайминг', + 'tab.diff': 'Разница', + 'tab.systemPrompt': 'Системная подсказка', + 'tab.tools': 'Инструменты', + 'tab.options': 'Опции', + 'tab.usage': 'Использование', + 'record.toolCallOnly': '(только вызов инструмента)', + 'record.noContent': 'Нет контента', + 'record.noPayload': 'Полезная нагрузка не захвачена', + 'record.noResult': 'Результат не зафиксирован', + 'record.noOutput': 'Нет вывода', + 'record.schemaUnavailable': 'Схема недоступна', + 'record.parameters': 'Параметры', + 'record.resultJson': 'Результат JSON', + 'record.json': 'JSON', + 'record.parametersJson': 'параметры JSON', + 'record.namedParametersJson': '{name} параметры JSON', + 'record.payloadJson': 'Полезная нагрузка JSON', + 'record.outputJson': 'Результат JSON', + 'record.thinking': 'Размышление', + 'record.wrapLines': 'Переносить строки', + 'code.source': 'Код', + 'code.output': 'Выход', + 'code.copySource': 'Скопировать код', + 'code.copyOutput': 'Копировать вывод', + 'code.originalJson': 'Исходный JSON', + 'code.running': 'Бег…', + 'record.systemPromptMissing': 'В этом запросе нет системного запроса', + 'record.toolsMissing': 'В этом запросе нет инструментов', + 'record.systemPrompt': 'Системная подсказка', + 'record.tools': 'Инструменты', + 'block.openSummary': 'Открыть сводку вызовов инструментов для блока №{index}', + 'block.openSummaryTitle': 'Открыть сводку вызовов инструментов', + 'block.label': 'Блок №{index} {type}', + 'history.loadingTrajectory': 'Загрузка траектории…', + 'history.loadingEarlier': 'Загрузка более ранней истории…', + 'history.loadingEarlierAria': 'Загрузка более ранней истории…', + 'history.loadEarlier': 'Загрузить предыдущую историю', + 'history.clickToLoadEarlier': 'Нажмите, чтобы загрузить предыдущую историю', + 'request.label': 'Запрос № {request}', + 'request.labelCompaction': 'Запрос №{request} · Сжатие', + 'request.compaction': 'Уплотнение · {section}', + 'request.compactionPurpose': 'Уплотнение', + 'request.retryProgress': '{retry} из {maximum}', + 'request.collapsedSummary': 'Свернутая сводка {kind}, {summary}', + 'request.collapsedTurn': 'ход', + 'request.collapsedAssistant': 'помощник', + 'request.rowAria': '{request}{kind}, {content}', + 'request.rowPrefix': 'Запрос {request}, ', + 'request.rowAriaCompaction': 'Запрос {request}, уплотнение', + 'request.noContent': 'нет содержания', + 'summary.toolCalls.one': '{count} вызов инструмента', + 'summary.toolCalls.other': '{count} вызовов инструментов', + 'summary.steps.one': '{count} шаг', + 'summary.steps.other': '{count} шагов', + 'details.event': 'Подробности мероприятия', + 'details.resize': 'Изменить размер сведений о мероприятии', + 'details.resizeTitle': 'Перетащите, чтобы изменить размер. Дважды щелкните для сброса.', + 'details.close': 'Закрыть детали', + 'details.status': 'Статус', + 'details.purpose': 'Цель', + 'details.provider': 'Поставщик', + 'details.model': 'Модель', + 'details.toolCalls': 'Вызовы инструментов', + 'details.subtoolCalls': 'Вызовы подинструментов', + 'details.error': 'Ошибка', + 'details.lastAttemptError': 'Ошибка последней попытки', + 'details.failure.auth': 'Ключ API недействителен.', + 'details.retry': 'Повторить', + 'details.retryDelay': 'Задержка повтора', + 'details.result': 'Результат', + 'details.compacted': 'Уплотненный', + 'details.assistantMessage': 'Сообщение помощника', + 'details.source': 'Источник', + 'details.hierarchy': 'Иерархия', + 'details.toolCall': 'Вызов инструмента', + 'timeline.aria': 'Хронология траектории', + 'timeline.overviewAria': 'Обзор временной шкалы; перетащите горизонтально, чтобы сфокусировать события', + 'timeline.noTimingData': 'Нет данных о времени', + 'timeline.total': 'Всего {duration}', + 'timeline.started': 'Запустил {time}', + 'timeline.ttftDecoding': 'TTFT {ttft} · Декодирование {decoding}', + 'layout.compacting': 'Сжатие контекста…', + 'layout.compactionFailed': 'Сжатие не удалось', + 'layout.compacted': 'Контекст сжат', + 'layout.toolCallOnly': 'Только вызов инструмента', + 'attachment.list': 'Вложения', + 'attachment.imageName': 'Изображение {index}', + 'layout.imageCount': 'Изображения ×{count}', + 'layout.fileAttachments': 'Файлы ×{count}', + 'layout.initialSystemPrompt': 'Начальная системная подсказка', + 'layout.systemPromptUpdated': 'Системная подсказка обновлена', + 'layout.toolsUpdated': 'Инструменты обновлены', + 'layout.toolAdded': 'Добавлен инструмент: {name}.', + 'layout.toolRemoved': 'Инструмент удален: {name}.', + 'layout.toolUpdateNotice': 'Инструменты обновлены', + 'layout.toolsAdded': 'Добавлено: {names}', + 'layout.toolsAddedCount': '{count} добавлен', + 'layout.toolsChanged': '{added} добавлен, {removed} удален.', + 'layout.toolsRemoved': 'Удалено: {names}.', + 'layout.toolsRemovedCount': '{count} удален.', + 'layout.systemPromptAndToolsUpdated': 'Системная подсказка и инструменты обновлены', + 'layout.compactionInterrupted': 'Уплотнение было прервано до завершения.', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'view.trajectory': 'Trajetória', + 'toolbar.aria': 'Barra de ferramentas Trajetória', + 'toolbar.duration': 'Duração', + 'toolbar.useActualDuration': 'Usar a duração real', + 'toolbar.useEqualWidth': 'Use operações de largura igual', + 'toolbar.actualTime': 'Hora real', + 'toolbar.turns': 'Turnos', + 'toolbar.expandTurns': 'Expandir turnos', + 'toolbar.collapseTurns': 'Recolher turnos', + 'toolbar.calls': 'Chamadas', + 'toolbar.expandCalls': 'Expandir chamadas', + 'toolbar.collapseCalls': 'Recolher chamadas', + 'toolbar.search': 'Trajetória de pesquisa', + 'toolbar.searchPlaceholder': 'Pesquisar', + 'kind.system': 'SISTEMA', + 'kind.user': 'USUÁRIO', + 'kind.context': 'CONTEXTO', + 'kind.compacted': 'COMPACTADO', + 'kind.message': 'Mensagem', + 'kind.assistant': 'ASSISTENTE', + 'kind.tool': 'FERRAMENTA', + 'kind.subtool': 'SUBFERRAMENTA', + 'kind.sub': 'Sub', + 'column.input': 'Entrada', + 'column.output': 'Saída', + 'column.think': 'Pense', + 'column.time': 'Hora', + 'column.model': 'Modelo', + 'column.tools': 'Ferramentas', + 'turn.label': 'Turno {turn}', + 'section.betweenTurns': 'Entre turnos', + 'group.message': 'Mensagem', + 'group.step': 'Etapa {step}', + 'group.compaction': 'Compactação {seq}', + 'status.failed': 'Falha', + 'status.pending': 'Pendente', + 'status.completed': 'Concluído', + 'timing.notAvailable': 'Não disponível', + 'timing.notRecorded': 'Não gravado', + 'timing.stepStartUnavailable': 'Início da etapa indisponível', + 'timing.firstTokenUnavailable': 'Primeiro token indisponível', + 'timing.usageUnavailable': 'Uso indisponível', + 'timing.outputTokensUnavailable': 'Tokens de saída indisponíveis', + 'timing.durationTooShort': 'Duração muito curta', + 'timing.showLocalTime': 'Mostrar hora local', + 'timing.showUnixTimestamp': 'Mostrar carimbo de data/hora Unix', + 'timing.started': 'Iniciado', + 'timing.totalDuration': 'Duração total', + 'timing.ttft': 'TTFT', + 'timing.generation': 'Geração', + 'timing.throughput': 'Taxa de transferência', + 'timing.duration': 'Duração', + 'timing.source': 'Fonte de tempo', + 'timing.sessionTimestamps': 'Carimbos de data e hora da sessão', + 'timing.sessionTimestampsRunning': 'Carimbos de data/hora da sessão (em execução)', + 'timing.request': 'Tempo de solicitação', + 'unit.milliseconds': '{value} ms', + 'unit.seconds': '{value} segundos', + 'unit.tokens': '{value} tok', + 'unit.tokensPerSecond': '{value} tok/s', + 'usage.tokens': 'Fichas', + 'usage.reasoning': 'Raciocínio', + 'usage.content': 'Conteúdo', + 'usage.notReported': 'Uso não relatado', + 'usage.input': 'Entrada', + 'usage.cached': 'Em cache', + 'usage.cacheCreated': 'Cache criado', + 'usage.other': 'Outro', + 'usage.output': 'Saída', + 'usage.thisRequest': 'Este pedido', + 'usage.sessionCumulative': 'Sessão cumulativa', + 'options.notRecorded': 'Opções não registradas', + 'options.json': 'Opções de solicitação JSON', + 'source.unknown': 'Desconhecido', + 'source.user': 'Usuário', + 'source.plugin': 'Plug-in', + 'source.pluginNamed': 'Plug-in · {plugin}', + 'source.goal': 'Objetivo', + 'source.goalRound': 'Meta · Rodada {round}', + 'source.notRecorded': 'Fonte não registrada', + 'source.messageJson': 'Fonte da mensagem JSON', + 'tab.summary': 'Resumo', + 'tab.rawOutput': 'Saída Bruta', + 'tab.preview': 'Visualização', + 'tab.raw': 'Cru', + 'tab.source': 'Fonte', + 'tab.payload': 'Carga útil', + 'tab.result': 'Resultado', + 'tab.schema': 'Esquema', + 'tab.timing': 'Tempo', + 'tab.diff': 'Diferença', + 'tab.systemPrompt': 'Alerta do sistema', + 'tab.tools': 'Ferramentas', + 'tab.options': 'Opções', + 'tab.usage': 'Uso', + 'record.toolCallOnly': '(somente chamada de ferramenta)', + 'record.noContent': 'Sem conteúdo', + 'record.noPayload': 'Nenhuma carga capturada', + 'record.noResult': 'Nenhum resultado capturado', + 'record.noOutput': 'Sem saída', + 'record.schemaUnavailable': 'Esquema indisponível', + 'record.parameters': 'Parâmetros', + 'record.resultJson': 'Resultado JSON', + 'record.json': 'JSON', + 'record.parametersJson': 'parâmetros JSON', + 'record.namedParametersJson': '{name} parâmetros JSON', + 'record.payloadJson': 'Carga útil JSON', + 'record.outputJson': 'Resultado JSON', + 'record.thinking': 'Pensando', + 'record.wrapLines': 'Quebrar linhas', + 'code.source': 'Código', + 'code.output': 'Saída', + 'code.copySource': 'Copiar código', + 'code.copyOutput': 'Copiar saída', + 'code.originalJson': 'JSON original', + 'code.running': 'Correndo…', + 'record.systemPromptMissing': 'Nenhum prompt do sistema nesta solicitação', + 'record.toolsMissing': 'Nenhuma ferramenta nesta solicitação', + 'record.systemPrompt': 'Alerta do sistema', + 'record.tools': 'Ferramentas', + 'block.openSummary': 'Resumo da chamada da ferramenta do bloco aberto #{index}', + 'block.openSummaryTitle': 'Resumo de chamada de ferramenta aberta', + 'block.label': 'Bloco nº{index} {type}', + 'history.loadingTrajectory': 'Carregando trajetória…', + 'history.loadingEarlier': 'Carregando histórico anterior…', + 'history.loadingEarlierAria': 'Carregando histórico anterior…', + 'history.loadEarlier': 'Carregar histórico anterior', + 'history.clickToLoadEarlier': 'Clique para carregar o histórico anterior', + 'request.label': 'Solicitação nº{request}', + 'request.labelCompaction': 'Solicitação nº{request} · Compactação', + 'request.compaction': 'Compactação · {section}', + 'request.compactionPurpose': 'Compactação', + 'request.retryProgress': '{retry} de {maximum}', + 'request.collapsedSummary': 'Resumo {kind} recolhido, {summary}', + 'request.collapsedTurn': 'turno', + 'request.collapsedAssistant': 'assistente', + 'request.rowAria': '{request}{kind}, {content}', + 'request.rowPrefix': 'Solicitação {request}, ', + 'request.rowAriaCompaction': 'Solicitação {request}, compactação', + 'request.noContent': 'sem conteúdo', + 'summary.toolCalls.one': '{count} chamada de ferramenta', + 'summary.toolCalls.other': '{count} chamadas de ferramenta', + 'summary.steps.one': '{count} etapa', + 'summary.steps.other': '{count} etapas', + 'details.event': 'Detalhes do evento', + 'details.resize': 'Redimensionar detalhes do evento', + 'details.resizeTitle': 'Arraste para redimensionar. Clique duas vezes para redefinir.', + 'details.close': 'Fechar detalhes', + 'details.status': 'Estado', + 'details.purpose': 'Objetivo', + 'details.provider': 'Provedor', + 'details.model': 'Modelo', + 'details.toolCalls': 'Chamadas de ferramentas', + 'details.subtoolCalls': 'Chamadas de subferramenta', + 'details.error': 'Erro', + 'details.lastAttemptError': 'Erro na última tentativa', + 'details.failure.auth': 'A chave API é inválida', + 'details.retry': 'Tentar novamente', + 'details.retryDelay': 'Atraso na nova tentativa', + 'details.result': 'Resultado', + 'details.compacted': 'Compactado', + 'details.assistantMessage': 'Mensagem do assistente', + 'details.source': 'Fonte', + 'details.hierarchy': 'Hierarquia', + 'details.toolCall': 'Chamada de ferramenta', + 'timeline.aria': 'Linha do tempo da trajetória', + 'timeline.overviewAria': 'Visão geral da linha do tempo; arraste horizontalmente para focar eventos', + 'timeline.noTimingData': 'Sem dados de tempo', + 'timeline.total': 'Total {duration}', + 'timeline.started': 'Iniciado {time}', + 'timeline.ttftDecoding': 'TTFT {ttft} · Decodificação {decoding}', + 'layout.compacting': 'Contexto compactado…', + 'layout.compactionFailed': 'Falha na compactação', + 'layout.compacted': 'Contexto compactado', + 'layout.toolCallOnly': 'Somente chamada de ferramenta', + 'attachment.list': 'Anexos', + 'attachment.imageName': 'Imagem {index}', + 'layout.imageCount': 'Imagens ×{count}', + 'layout.fileAttachments': 'Arquivos ×{count}', + 'layout.initialSystemPrompt': 'Prompt inicial do sistema', + 'layout.systemPromptUpdated': 'Prompt do sistema atualizado', + 'layout.toolsUpdated': 'Ferramentas atualizadas', + 'layout.toolAdded': 'Ferramenta adicionada: {name}', + 'layout.toolRemoved': 'Ferramenta removida: {name}', + 'layout.toolUpdateNotice': 'Ferramentas atualizadas', + 'layout.toolsAdded': 'Adicionado: {names}', + 'layout.toolsAddedCount': '{count} adicionado', + 'layout.toolsChanged': '{added} adicionado, {removed} removido', + 'layout.toolsRemoved': 'Removido: {names}', + 'layout.toolsRemovedCount': '{count} removido', + 'layout.systemPromptAndToolsUpdated': 'Prompt do sistema e ferramentas atualizadas', + 'layout.compactionInterrupted': 'A compactação foi interrompida antes da conclusão.', +} satisfies Record diff --git a/packages/client/ui-trajectory/src/client/views.module.css b/packages/client/ui-trajectory/src/client/views.module.css index 687f4a4657..c7c963a5b2 100644 --- a/packages/client/ui-trajectory/src/client/views.module.css +++ b/packages/client/ui-trajectory/src/client/views.module.css @@ -1,7 +1,11 @@ -/* Full-bleed, fixed-height host for the trajectory ledger. */ +/* Full-bleed, fixed-height host for the trajectory ledger. Isolation keeps the + sticky toolbar's z-index inside this view, so the conversation tab row + stacked above the view still paints its active bar over the view's top + edge. */ .root { --dsh-trajectory-toolbar-height: 32px; + isolation: isolate; display: flex; flex-direction: column; overflow: hidden; diff --git a/packages/client/ui-trajectory/tests/table.client.spec.tsx b/packages/client/ui-trajectory/tests/table.client.spec.tsx index d847f29dc1..0789d4ae97 100644 --- a/packages/client/ui-trajectory/tests/table.client.spec.tsx +++ b/packages/client/ui-trajectory/tests/table.client.spec.tsx @@ -1,3 +1,5 @@ +import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' +import { es, pt } from '../src/client/locales.ts' // @vitest-environment jsdom /** Trajectory ledger selection, details, status, and fold behavior. */ @@ -1350,3 +1352,8 @@ describe('TrajectoryTable', () => { expect(onInspectApplied).not.toHaveBeenCalled() }) }) + +it('preserves seconds and token units in translated metric labels', () => { + expect(makeTranslate(es)('unit.seconds', { value: '1.25' })).toBe('1.25 s') + expect(makeTranslate(pt)('unit.tokens', { value: 42 })).toBe('42 tok') +}) diff --git a/packages/client/ui-user-questions/locale/de.json b/packages/client/ui-user-questions/locale/de.json new file mode 100644 index 0000000000..363d951575 --- /dev/null +++ b/packages/client/ui-user-questions/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Benutzeroberfläche für Fragen und Antworten", + "description": "Präsentieren Sie die Fragen der KI, planen Sie Bewertungsanfragen in einem Gespräch und sammeln Sie Benutzerantworten." + } +} diff --git a/packages/client/ui-user-questions/locale/es.json b/packages/client/ui-user-questions/locale/es.json new file mode 100644 index 0000000000..d6574488e9 --- /dev/null +++ b/packages/client/ui-user-questions/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de preguntas y respuestas del usuario", + "description": "Presente las preguntas de la IA, programe solicitudes de revisión en una conversación y recopile las respuestas de los usuarios." + } +} diff --git a/packages/client/ui-user-questions/locale/fr.json b/packages/client/ui-user-questions/locale/fr.json new file mode 100644 index 0000000000..b8fcde16e9 --- /dev/null +++ b/packages/client/ui-user-questions/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de questions et réponses utilisateur", + "description": "Présentez les questions de l'IA et planifiez les demandes de révision dans une conversation, et collectez les réponses des utilisateurs." + } +} diff --git a/packages/client/ui-user-questions/locale/pt.json b/packages/client/ui-user-questions/locale/pt.json new file mode 100644 index 0000000000..0e14cc5897 --- /dev/null +++ b/packages/client/ui-user-questions/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de perguntas e respostas do usuário", + "description": "Apresente as perguntas da IA, agende solicitações de revisão em uma conversa e colete as respostas dos usuários." + } +} diff --git a/packages/client/ui-user-questions/locale/ru.json b/packages/client/ui-user-questions/locale/ru.json new file mode 100644 index 0000000000..dcaf1ef773 --- /dev/null +++ b/packages/client/ui-user-questions/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Пользовательский интерфейс вопросов и ответов", + "description": "Задавайте вопросы ИИ и планируйте запросы на проверку в беседе, а также собирайте ответы пользователей." + } +} diff --git a/packages/client/ui-user-questions/src/client/index.ts b/packages/client/ui-user-questions/src/client/index.ts index 618cb599af..0f8255d6f9 100644 --- a/packages/client/ui-user-questions/src/client/index.ts +++ b/packages/client/ui-user-questions/src/client/index.ts @@ -30,7 +30,7 @@ import { createQuestionDraftStore } from './draft-store.ts' import { QuestionComposer } from './QuestionComposer.tsx' import { answeredQuestionDefinition, questionReplyDefinition } from './question-reply.ts' import { QuestionReplyView } from './QuestionReplyView.tsx' -import { en, ja, zh, type QuestionKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type QuestionKey } from './locales.ts' export type { PendingQuestion, PlanReview, QuestionAnswer, QuestionComposerProps, QuestionWait, @@ -366,7 +366,7 @@ function publishContinuedQuestions(ctx: ClientContext, cards: QuestionCards): () * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-user-questions: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-user-questions: dictionaries') const questionDraftStore = createQuestionDraftStore() const registerPendingInteraction = ctx.uiSession.registerPendingInteraction( pending => pending.kind === 'plan-review' ? 2 : 1, diff --git a/packages/client/ui-user-questions/src/client/locales.ts b/packages/client/ui-user-questions/src/client/locales.ts index 719e4ecd0b..4a968bbf51 100644 --- a/packages/client/ui-user-questions/src/client/locales.ts +++ b/packages/client/ui-user-questions/src/client/locales.ts @@ -107,3 +107,178 @@ export const ja = { 'plan.decline': '拒否', 'plan.discuss': '変更を依頼', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'error.incomplete': 'Por favor complete esta pregunta primero.', + 'error.unanswered': 'Seleccione una opción o ingrese una respuesta personalizada.', + 'error.unavailable': 'No se puede enviar en este momento; Inténtalo de nuevo en un momento.', + 'error.resubmit': 'La respuesta no llegó antes de que continuara el trabajo; enviarlo de nuevo.', + 'status.sent': 'Respuesta enviada; el panel no pudo cerrarse.', + 'wait.takeTime': 'tómate el tiempo', + 'wait.countdown': 'Continuando en {seconds}s', + 'wait.paused': 'En pausa · {seconds} restantes', + 'wait.held': 'Esperando hasta que respondas', + 'wait.continued': 'El trabajo continúa; aún puedes responder', + 'review.status': 'Respondido', + 'review.skipped': 'Esta pregunta fue omitida.', + 'reply.label': 'Preguntas y respuestas', + 'reply.answerLabel': 'Respuesta: ', + 'reply.skipped': 'Saltado', + 'nav.prev': 'Pregunta anterior', + 'nav.next': 'Siguiente pregunta', + 'nav.minimize': 'Contraer la tarjeta de preguntas', + 'nav.maximize': 'Ampliar la tarjeta de preguntas', + 'nav.cancel': 'Descartar todas las preguntas', + 'nav.close': 'Cierre el panel; vuelva a abrirlo desde la llamada de la herramienta', + 'text.expand': 'Expandir', + 'text.collapse': 'Contraer', + 'option.recommended': 'Recomendado', + 'custom.placeholder': 'Escribe tu respuesta', + 'action.skip': 'Saltar', + 'action.next': 'Siguiente', + 'plan.header': 'Revisión del plan', + 'plan.approve': 'Aprobar', + 'plan.decline': 'Rechazar', + 'plan.discuss': 'Solicitar cambios', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'error.incomplete': "Veuillez d'abord répondre à cette question.", + 'error.unanswered': 'Veuillez sélectionner une option ou saisir une réponse personnalisée.', + 'error.unavailable': 'Impossible de soumettre pour le moment ; réessayez dans un instant.', + 'error.resubmit': 'La réponse n’est pas arrivée avant la poursuite des travaux ; soumettez-le à nouveau.', + 'status.sent': "Réponse envoyée ; le panneau n'a pas pu se fermer.", + 'wait.takeTime': 'Prends le temps', + 'wait.countdown': 'Poursuite dans {seconds} s', + 'wait.paused': 'En pause · {seconds} restants', + 'wait.held': 'En attente de votre réponse', + 'wait.continued': "Le travail s'est poursuivi — vous pouvez toujours répondre", + 'review.status': 'Répondu', + 'review.skipped': 'Cette question a été ignorée.', + 'reply.label': 'Questions et réponses', + 'reply.answerLabel': 'Réponse : ', + 'reply.skipped': 'Skipped', + 'nav.prev': 'Question précédente', + 'nav.next': 'Question suivante', + 'nav.minimize': 'Réduire la fiche de questions', + 'nav.maximize': 'Développer la carte de question', + 'nav.cancel': 'Ignorer toutes les questions', + 'nav.close': "Fermez le panneau - rouvrez-le à partir de l'appel de l'outil", + 'text.expand': 'Développer', + 'text.collapse': 'Réduire', + 'option.recommended': 'Recommandé', + 'custom.placeholder': 'Saisissez votre réponse', + 'action.skip': 'Sauter', + 'action.next': 'Suivant', + 'plan.header': 'Révision des plans', + 'plan.approve': 'Approuver', + 'plan.decline': 'Refuser', + 'plan.discuss': 'Demander des modifications', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'error.incomplete': 'Bitte füllen Sie zunächst diese Frage aus.', + 'error.unanswered': 'Bitte wählen Sie eine Option aus oder geben Sie eine benutzerdefinierte Antwort ein.', + 'error.unavailable': 'Kann momentan nicht eingereicht werden; Versuchen Sie es gleich noch einmal.', + 'error.resubmit': 'Die Antwort kam erst, als die Arbeiten weitergingen; reichen Sie es erneut ein.', + 'status.sent': 'Antwort gesendet; Das Panel konnte nicht geschlossen werden.', + 'wait.takeTime': 'Nehmen Sie sich Zeit', + 'wait.countdown': 'Fortsetzung in {seconds}s', + 'wait.paused': 'Angehalten · Verbleibende {seconds}s', + 'wait.held': 'Ich warte, bis du antwortest', + 'wait.continued': 'Die Arbeit ging weiter – Sie können immer noch antworten', + 'review.status': 'Beantwortet', + 'review.skipped': 'Diese Frage wurde übersprungen.', + 'reply.label': 'Fragen und Antworten', + 'reply.answerLabel': 'Antwort: ', + 'reply.skipped': 'Übersprungen', + 'nav.prev': 'Vorherige Frage', + 'nav.next': 'Nächste Frage', + 'nav.minimize': 'Reduzieren Sie die Fragenkarte', + 'nav.maximize': 'Erweitern Sie die Fragenkarte', + 'nav.cancel': 'Alle Fragen ablehnen', + 'nav.close': 'Schließen Sie das Panel – öffnen Sie es erneut über den Tool-Aufruf', + 'text.expand': 'Ausklappen', + 'text.collapse': 'Einklappen', + 'option.recommended': 'Empfohlen', + 'custom.placeholder': 'Geben Sie Ihre Antwort ein', + 'action.skip': 'Überspringen', + 'action.next': 'Weiter', + 'plan.header': 'Planüberprüfung', + 'plan.approve': 'Genehmigen', + 'plan.decline': 'Ablehnen', + 'plan.discuss': 'Fordern Sie Änderungen an', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'error.incomplete': 'Пожалуйста, сначала ответьте на этот вопрос.', + 'error.unanswered': 'Пожалуйста, выберите вариант или введите собственный ответ.', + 'error.unavailable': 'Невозможно отправить прямо сейчас; попробуйте еще раз через минуту.', + 'error.resubmit': 'Ответ не пришел до продолжения работы; отправьте его еще раз.', + 'status.sent': 'Ответ отправлен; панель не могла закрыться.', + 'wait.takeTime': 'Найдите время', + 'wait.countdown': 'Продолжение в {seconds}s', + 'wait.paused': 'Приостановлено · Осталось {seconds}', + 'wait.held': 'Жду, пока ты ответишь', + 'wait.continued': 'Работа продолжается — вы еще можете ответить', + 'review.status': 'Ответил', + 'review.skipped': 'Этот вопрос был пропущен.', + 'reply.label': 'Вопросы и ответы', + 'reply.answerLabel': 'Ответ: ', + 'reply.skipped': 'Пропущено', + 'nav.prev': 'Предыдущий вопрос', + 'nav.next': 'Следующий вопрос', + 'nav.minimize': 'Свернуть карточку с вопросом', + 'nav.maximize': 'Развернуть карточку с вопросом', + 'nav.cancel': 'Отклонить все вопросы', + 'nav.close': 'Закрыть панель — открыть ее снова из вызова инструмента', + 'text.expand': 'Развернуть', + 'text.collapse': 'Свернуть', + 'option.recommended': 'Рекомендуется', + 'custom.placeholder': 'Введите ответ', + 'action.skip': 'Пропустить', + 'action.next': 'Далее', + 'plan.header': 'Обзор плана', + 'plan.approve': 'Утвердить', + 'plan.decline': 'Отказаться', + 'plan.discuss': 'Запросить изменения', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'error.incomplete': 'Por favor, responda esta pergunta primeiro.', + 'error.unanswered': 'Selecione uma opção ou insira uma resposta personalizada.', + 'error.unavailable': 'Não é possível enviar agora; tente novamente em um momento.', + 'error.resubmit': 'A resposta não chegou antes da continuação do trabalho; envie-o novamente.', + 'status.sent': 'Resposta enviada; o painel não pôde fechar.', + 'wait.takeTime': 'Reserve um tempo', + 'wait.countdown': 'Continuando em {seconds}s', + 'wait.paused': 'Pausado · {seconds}s restantes', + 'wait.held': 'Esperando até você responder', + 'wait.continued': 'O trabalho continuou – você ainda pode responder', + 'review.status': 'Respondido', + 'review.skipped': 'Esta questão foi ignorada.', + 'reply.label': 'Perguntas e respostas', + 'reply.answerLabel': 'Resposta: ', + 'reply.skipped': 'Ignorado', + 'nav.prev': 'Pergunta anterior', + 'nav.next': 'Próxima pergunta', + 'nav.minimize': 'Recolher o cartão de perguntas', + 'nav.maximize': 'Expanda o cartão de perguntas', + 'nav.cancel': 'Dispense todas as perguntas', + 'nav.close': 'Feche o painel – reabra-o na chamada da ferramenta', + 'text.expand': 'Expandir', + 'text.collapse': 'Recolher', + 'option.recommended': 'Recomendado', + 'custom.placeholder': 'Digite sua resposta', + 'action.skip': 'Pular', + 'action.next': 'Próximo', + 'plan.header': 'Revisão do plano', + 'plan.approve': 'Aprovar', + 'plan.decline': 'Recusar', + 'plan.discuss': 'Solicitar alterações', +} satisfies Record diff --git a/packages/client/ui-workflow-run/locale/de.json b/packages/client/ui-workflow-run/locale/de.json new file mode 100644 index 0000000000..7023c7a420 --- /dev/null +++ b/packages/client/ui-workflow-run/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Anzeige der Workflow-Ausführung", + "description": "Zeigen Sie Workflow-Ausführungsdatensätze und ihre Mitgliedsdetails in der Sitzung an." + } +} diff --git a/packages/client/ui-workflow-run/locale/es.json b/packages/client/ui-workflow-run/locale/es.json new file mode 100644 index 0000000000..866a8b82a1 --- /dev/null +++ b/packages/client/ui-workflow-run/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Visualización de ejecución del flujo de trabajo", + "description": "Muestre registros de ejecución de flujo de trabajo y los detalles de sus miembros en la sesión." + } +} diff --git a/packages/client/ui-workflow-run/locale/fr.json b/packages/client/ui-workflow-run/locale/fr.json new file mode 100644 index 0000000000..34d095b8bf --- /dev/null +++ b/packages/client/ui-workflow-run/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Affichage de l'exécution du workflow", + "description": "Affichez les enregistrements d'exécution de flux de travail et les détails de leurs membres dans la session." + } +} diff --git a/packages/client/ui-workflow-run/locale/pt.json b/packages/client/ui-workflow-run/locale/pt.json new file mode 100644 index 0000000000..403aca3a74 --- /dev/null +++ b/packages/client/ui-workflow-run/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Exibição de execução de fluxo de trabalho", + "description": "Exiba registros de execução de fluxo de trabalho e detalhes de seus membros na sessão." + } +} diff --git a/packages/client/ui-workflow-run/locale/ru.json b/packages/client/ui-workflow-run/locale/ru.json new file mode 100644 index 0000000000..dd7ede4381 --- /dev/null +++ b/packages/client/ui-workflow-run/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Отображение выполнения рабочего процесса", + "description": "Отображение записей выполнения рабочего процесса и сведений об их участниках в сеансе." + } +} diff --git a/packages/client/ui-workflow-run/src/client/index.ts b/packages/client/ui-workflow-run/src/client/index.ts index b3da13c944..648dbd1f46 100644 --- a/packages/client/ui-workflow-run/src/client/index.ts +++ b/packages/client/ui-workflow-run/src/client/index.ts @@ -9,7 +9,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type {} from '@deepseek-ai/dsh-client-ui-session/client' import type {} from '@deepseek-ai/dsh-client-ui-workspace/client' import { WorkflowRunPanel, type WorkflowRunInjected } from './WorkflowRunPanel.tsx' -import { en, ja, NS, type WorkflowRunKey, zh } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, NS, type WorkflowRunKey, zh } from './locales.ts' import { workflowRunDefinition } from './workflow-definition.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { @@ -25,7 +25,7 @@ export const inject = ['uiConversation', 'uiWorkspace', 'slots', 'sessions', 'lo /** Register the workflow Definition, dictionary, and keyed Chat renderer. */ export function apply(ctx: ClientContext): void { ctx.uiConversation.events.register(workflowRunDefinition) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-workflow-run: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-workflow-run: dictionaries') ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({ name: 'conversation.chat.node', key: 'workflow-run', diff --git a/packages/client/ui-workflow-run/src/client/locales.ts b/packages/client/ui-workflow-run/src/client/locales.ts index 8243740ca8..ad0224ecc7 100644 --- a/packages/client/ui-workflow-run/src/client/locales.ts +++ b/packages/client/ui-workflow-run/src/client/locales.ts @@ -71,3 +71,113 @@ export const ja = { 'status.cancelled': 'キャンセル', 'status.interrupted': '中断', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'run.title': '{name}', + 'run.members.one': 'Miembro de {count}', + 'run.members.other': 'Miembros de {count}', + 'run.empty': 'Ningún miembro comenzó', + 'phase.unassigned': 'sin fases', + 'phase.empty': 'Nombre de fase vacío', + 'statusCount.running': 'Ejecutando {count}', + 'statusCount.completed': 'Completado {count}', + 'statusCount.failed': 'Error {count}', + 'statusCount.cancelled': 'Cancelado {count}', + 'statusCount.interrupted': 'Interrumpido {count}', + 'member.empty': 'Nombre de miembro vacío', + 'member.open': 'Abrir {name}', + 'status.running': 'corriendo', + 'status.completed': 'Completado', + 'status.failed': 'Fallido', + 'status.cancelled': 'Cancelado', + 'status.interrupted': 'interrumpido', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'run.title': '{name}', + 'run.members.one': 'Membre {count}', + 'run.members.other': 'Membres {count}', + 'run.empty': "Aucun membre n'a commencé", + 'phase.unassigned': 'Sans phase', + 'phase.empty': 'Nom de phase vide', + 'statusCount.running': 'Exécution de {count}', + 'statusCount.completed': 'Terminé {count}', + 'statusCount.failed': 'Échec du {count}', + 'statusCount.cancelled': 'Annulé {count}', + 'statusCount.interrupted': '{count} interrompu', + 'member.empty': 'Nom du membre vide', + 'member.open': 'Ouvrir {name}', + 'status.running': 'Courir', + 'status.completed': 'Terminé', + 'status.failed': 'Échec', + 'status.cancelled': 'Annulé', + 'status.interrupted': 'Interrompu', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'run.title': '{name}', + 'run.members.one': '{count}-Mitglied', + 'run.members.other': '{count}-Mitglieder', + 'run.empty': 'Keine Mitglieder gestartet', + 'phase.unassigned': 'Unphasiert', + 'phase.empty': 'Leerer Phasenname', + 'statusCount.running': 'Ausführen von {count}', + 'statusCount.completed': '{count} abgeschlossen', + 'statusCount.failed': 'Fehlgeschlagen {count}', + 'statusCount.cancelled': 'Storniert {count}', + 'statusCount.interrupted': 'Unterbrochen {count}', + 'member.empty': 'Leerer Mitgliedsname', + 'member.open': 'Öffnen Sie {name}', + 'status.running': 'Laufen', + 'status.completed': 'Abgeschlossen', + 'status.failed': 'Fehlgeschlagen', + 'status.cancelled': 'Abgesagt', + 'status.interrupted': 'Unterbrochen', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'run.title': '{name}', + 'run.members.one': 'Участник {count}', + 'run.members.other': 'Участники {count}', + 'run.empty': 'Ни один из участников не стартовал', + 'phase.unassigned': 'Нефазированный', + 'phase.empty': 'Пустое имя фазы', + 'statusCount.running': 'Запуск {count}', + 'statusCount.completed': 'Завершено {count}', + 'statusCount.failed': 'Ошибка {count}', + 'statusCount.cancelled': 'Отменено {count}', + 'statusCount.interrupted': 'Прервано {count}', + 'member.empty': 'Пустое имя участника', + 'member.open': 'Открыть {name}', + 'status.running': 'Бег', + 'status.completed': 'Завершено', + 'status.failed': 'Не удалось', + 'status.cancelled': 'Отменено', + 'status.interrupted': 'Прерванный', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'run.title': '{name}', + 'run.members.one': '{count} membro', + 'run.members.other': '{count} membros', + 'run.empty': 'Nenhum membro começou', + 'phase.unassigned': 'Sem fases', + 'phase.empty': 'Nome da fase vazia', + 'statusCount.running': 'Executando {count}', + 'statusCount.completed': 'Concluído {count}', + 'statusCount.failed': 'Falha {count}', + 'statusCount.cancelled': 'Cancelado {count}', + 'statusCount.interrupted': 'Interrompido {count}', + 'member.empty': 'Nome de membro vazio', + 'member.open': 'Abra {name}', + 'status.running': 'Correndo', + 'status.completed': 'Concluído', + 'status.failed': 'Falha', + 'status.cancelled': 'Cancelado', + 'status.interrupted': 'Interrompido', +} satisfies Record diff --git a/packages/client/ui-workspace/README.i18n.yaml b/packages/client/ui-workspace/README.i18n.yaml index 3d826e05b2..2db12fdf09 100644 --- a/packages/client/ui-workspace/README.i18n.yaml +++ b/packages/client/ui-workspace/README.i18n.yaml @@ -33,8 +33,8 @@ en: 9fb88260380bf093 zh: 3bace2b96ecd4a82 /deepseek-ai-dsh-client-ui-workspace/use-this-package/active-schedule-markers: - en: ac15c05cb045f748 - zh: 6330172c7231b2b8 + en: 2168316f604893a5 + zh: fe11347009339a9a /deepseek-ai-dsh-client-ui-workspace/understand-the-implementation: en: 96a37e74555a36b7 zh: d03941523509a5cf diff --git a/packages/client/ui-workspace/README.md b/packages/client/ui-workspace/README.md index 754428e4e8..5048b50c93 100644 --- a/packages/client/ui-workspace/README.md +++ b/packages/client/ui-workspace/README.md @@ -73,7 +73,9 @@ Grouped and flat Session rows show a clock mark when the Session has active sche ----- -`ctx.uiWorkspace.openSession(target)` synchronously replaces the owned `mainView` reference and returns the main area to Conversation without waiting for `reference.ready`, so history loading renders inside the selected Session view. The target may be a known Session id or a durable direct-parent subagent address; an explicit address does not require a preloaded parent catalog. `openWorkspace(id, beforeOpen?, options?)` opens a Workspace blank only if no later navigation has superseded the request; New Session opens the same blank as Workspace reopening, so it returns to that blank's unsent draft. `forkSession(id)` creates the child without navigating or superseding a pending navigation. The optional synchronous preparation callback runs after the target is retained and only for a current Workspace request, so superseded requests do not move composer drafts. Navigation or owner disposal suppresses the late UI commit, not the underlying Session creation. Startup restoration retains its main reference without changing the selected panel or cancelling a later navigation. Archiving the main Session releases its reference and clears the main selection. Selection failure leaves a global panel visible. Session rows read `usePanelInfo` to suppress their selected appearance while a global panel is active; search and directory-picker focus alone do not leave that panel. +`ctx.uiWorkspace.openSession(target)` synchronously replaces the owned `mainView` reference and returns the main area to Conversation without waiting for `reference.ready`, so history loading renders inside the selected Session view. The target may be a known Session id or a durable direct-parent subagent address; an explicit address does not require a preloaded parent catalog. `openWorkspace(id, beforeOpen?, options?)` opens a Workspace blank only if no later navigation has superseded the request; New Session opens the same blank as Workspace reopening, so it returns to that blank's unsent draft. `forkSession(id, onCreated?, options?)` creates the child without navigating or superseding a pending navigation; `allowMigration` carries the caller's migration permission. The optional synchronous preparation callback runs after the target is retained and only for a current Workspace request, so superseded requests do not move composer drafts. Navigation or owner disposal suppresses the late UI commit, not the underlying Session creation. + +Session and Workspace rows derive their affordances from the browser root's access Hooks: the global `useRemoteAccess('session/create')` gates every New Session entry, `useWorkspaceAccess` gates a Workspace's create, rename, delete, and reorder verbs, and `useSessionAccess` gates a Session row's rename, fork, drag, pin, archive, and unarchive verbs plus the search result's restore control. A refused verb is not rendered, while every Host request remains authoritative. Startup restoration retains its main reference without changing the selected panel or cancelling a later navigation. Archiving the main Session releases its reference and clears the main selection. Selection failure leaves a global panel visible. Session rows read `usePanelInfo` to suppress their selected appearance while a global panel is active; search and directory-picker focus alone do not leave that panel. Navigation and startup restoration obtain the selected Session's projections from its `follow`; neither issues a separate projection refresh for that Session or its parent, except that restoring a saved blank first reloads that Session's projections, because listed values are cached hints that can lag its log after an unclean Host exit. diff --git a/packages/client/ui-workspace/README.zh.md b/packages/client/ui-workspace/README.zh.md index 34da2eb9b8..e28bcfda6e 100644 --- a/packages/client/ui-workspace/README.zh.md +++ b/packages/client/ui-workspace/README.zh.md @@ -73,7 +73,9 @@ Session 行渲染运行时的实时 `pendingInteraction` 分类:审批显示** ----- -`ctx.uiWorkspace.openSession(target)` 会同步替换其拥有的 `mainView` reference,并让主区域返回 Conversation,而不等待 `reference.ready`,因此历史加载会显示在已经选中的 Session 视图内。目标可以是已知 Session id,也可以是持久的直接父子 subagent 地址;显式地址不要求预先加载 parent catalog。`openWorkspace(id, beforeOpen?, options?)` 仅在请求未被后续导航替代时打开工作区空白 Session;新会话与重新打开工作区打开同一个空白会话,因此会回到该空白会话中未发送的草稿。`forkSession(id)` 创建子会话,不导航,也不替代尚未完成的导航。可选的同步准备回调在目标被 retain 后执行,并且仅对仍有效的 Workspace 请求执行,因此过期请求不会搬移 composer 草稿。后续导航或 owner 释放会阻止晚到的 UI 提交,但不取消底层 Session 创建。启动恢复会 retain 主 reference,不改变已选面板,也不取消后续导航。归档主 Session 会释放其 reference 并清除主选择。选择失败时保留当前全局面板。Session 行读取 `usePanelInfo`,在全局面板活跃时不显示 Session 选中样式;仅把焦点移到搜索框或目录选择器不会离开该面板。 +`ctx.uiWorkspace.openSession(target)` 会同步替换其拥有的 `mainView` reference,并让主区域返回 Conversation,而不等待 `reference.ready`,因此历史加载会显示在已经选中的 Session 视图内。目标可以是已知 Session id,也可以是持久的直接父子 subagent 地址;显式地址不要求预先加载 parent catalog。`openWorkspace(id, beforeOpen?, options?)` 仅在请求未被后续导航替代时打开工作区空白 Session;新会话与重新打开工作区打开同一个空白会话,因此会回到该空白会话中未发送的草稿。`forkSession(id, onCreated?, options?)` 创建子会话,不导航,也不替代尚未完成的导航;`allowMigration` 携带调用方的迁移许可。可选的同步准备回调在目标被 retain 后执行,并且仅对仍有效的 Workspace 请求执行,因此过期请求不会搬移 composer 草稿。后续导航或 owner 释放会阻止晚到的 UI 提交,但不取消底层 Session 创建。 + +Session 行与 Workspace 行的可用操作来自浏览器根的访问 Hook:全局 `useRemoteAccess('session/create')` 门控所有新建会话入口,`useWorkspaceAccess` 门控 Workspace 的新建、重命名、删除与排序动作,`useSessionAccess` 门控 Session 行的重命名、分叉、拖拽、置顶、归档与取消归档动作,以及搜索结果的恢复控件。被拒绝的动作不渲染,但每个发往 Host 的请求仍以其授权为准。启动恢复会 retain 主 reference,不改变已选面板,也不取消后续导航。归档主 Session 会释放其 reference 并清除主选择。选择失败时保留当前全局面板。Session 行读取 `usePanelInfo`,在全局面板活跃时不显示 Session 选中样式;仅把焦点移到搜索框或目录选择器不会离开该面板。 导航和启动恢复通过所选 Session 的 `follow` 获取投影,不会另行刷新该 Session 或其父会话的投影;例外是恢复已保存的空白会话时先重新加载该 Session 的投影,因为列表中的值是缓存提示,Host 非正常退出后可能落后于其日志。 diff --git a/packages/client/ui-workspace/locale/de.json b/packages/client/ui-workspace/locale/de.json new file mode 100644 index 0000000000..0d85597bbb --- /dev/null +++ b/packages/client/ui-workspace/locale/de.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Schnittstelle zur Arbeitsbereichsauswahl", + "description": "Wählen Sie den Arbeitsbereich in der Seitenleiste und auf der Seite „Neue Sitzung“ aus." + } +} diff --git a/packages/client/ui-workspace/locale/es.json b/packages/client/ui-workspace/locale/es.json new file mode 100644 index 0000000000..761307af3f --- /dev/null +++ b/packages/client/ui-workspace/locale/es.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interfaz de selección de espacio de trabajo", + "description": "Seleccione el espacio de trabajo en la barra lateral y en la página de nueva sesión." + } +} diff --git a/packages/client/ui-workspace/locale/fr.json b/packages/client/ui-workspace/locale/fr.json new file mode 100644 index 0000000000..7de0b3ed4f --- /dev/null +++ b/packages/client/ui-workspace/locale/fr.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de sélection d'espace de travail", + "description": "Sélectionnez l'espace de travail dans la barre latérale et la page de nouvelle session." + } +} diff --git a/packages/client/ui-workspace/locale/pt.json b/packages/client/ui-workspace/locale/pt.json new file mode 100644 index 0000000000..5fbce0a63a --- /dev/null +++ b/packages/client/ui-workspace/locale/pt.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Interface de seleção de espaço de trabalho", + "description": "Selecione o espaço de trabalho na barra lateral e na página da nova sessão." + } +} diff --git a/packages/client/ui-workspace/locale/ru.json b/packages/client/ui-workspace/locale/ru.json new file mode 100644 index 0000000000..bfca23e851 --- /dev/null +++ b/packages/client/ui-workspace/locale/ru.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Интерфейс выбора рабочего пространства", + "description": "Выберите рабочую область на боковой панели и страницу нового сеанса." + } +} diff --git a/packages/client/ui-workspace/src/client/index.ts b/packages/client/ui-workspace/src/client/index.ts index 94199e9111..56985d0872 100644 --- a/packages/client/ui-workspace/src/client/index.ts +++ b/packages/client/ui-workspace/src/client/index.ts @@ -56,7 +56,7 @@ import { RowActionToast } from './session-actions/RowActionToast.tsx' import { registerToasts } from './toast/register.ts' import { SessionIdMenuItem } from './session-actions/SessionId.tsx' import { WorkspacePicker } from './WorkspacePicker.tsx' -import { en, ja, zh, type WorkspaceKey } from './locales.ts' +import { en, ja, es, fr, de, ru, pt, zh, type WorkspaceKey } from './locales.ts' export type { StartSessionOptions, UiWorkspace } from './navigation.ts' export type { @@ -132,7 +132,7 @@ export function apply(ctx: Context): void { : ctx.remoteAccess.source('workspace', workspaceId, action), }, }) - ctx.effect(() => ctx.locale.register(NS, { zh, en, ja }), 'ui-workspace: dictionaries') + ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, es, fr, de, ru, pt }), 'ui-workspace: dictionaries') const shortcutControls = createWorkspaceShortcutControls() const searchSessions: WorkspaceBrowserInjected['searchSessions'] = async (query, signal) => { diff --git a/packages/client/ui-workspace/src/client/locales.ts b/packages/client/ui-workspace/src/client/locales.ts index 938b893b40..7420f4547b 100644 --- a/packages/client/ui-workspace/src/client/locales.ts +++ b/packages/client/ui-workspace/src/client/locales.ts @@ -375,3 +375,618 @@ export const ja = { 'time.years': '{n}年', 'time.ago': '{t}前', } satisfies Record + +/** Spanish dictionary, checked against the English keys. */ +export const es = { + 'defaultWorkspace.failed': 'No se puede crear el espacio de trabajo predeterminado. Utilice Elegir espacio de trabajo para seleccionar una carpeta.', + 'draft.workspaceRequired': 'Elija un espacio de trabajo primero.', + 'draft.initializationFailed': 'No se pudo completar el borrador. Inténtelo de nuevo en breve.', + 'group.ungrouped': 'Desagrupados', + 'session.new': 'Nueva sesión', + 'session.untitled': 'Sin título', + 'shortcut.noSession': 'Seleccione una sesión primero', + 'shortcut.noPicker': 'Selector de directorio no disponible', + 'shortcut.directoryBusy': 'Seleccionar o agregar un espacio de trabajo', + 'shortcut.noCompletedTurn': 'Esta sesión no tiene turno completo', + 'shortcut.forkFailed': 'No se pudo bifurcar la sesión. Intentar otra vez.', + 'section.workspaces': 'Espacios de trabajo', + 'section.sessions': 'Sesiones', + 'viewOptions.label': 'Ver opciones', + 'groupBy.label': 'Agrupar por', + 'groupBy.workspace': 'Espacio de trabajo', + 'groupBy.workspaceTree': 'Árbol del espacio de trabajo', + 'groupBy.flat': 'en una lista', + 'orderBy.label': 'Ordenar por', + 'orderBy.manual': 'manuales', + 'orderBy.updated': 'Última actualización', + 'filterBy.label': 'Filtrar sesiones', + 'viewOptions.hideArchived': 'Ocultar archivado', + 'viewOptions.showArchived': 'Todas las conversaciones (mostrar archivadas)', + 'viewOptions.onlyArchived': 'Sólo archivado', + 'sessions.expand': 'Mostrar {n} más sesiones', + 'sessions.collapse': 'Mostrar menos', + 'empty.none': 'Aún no hay sesiones', + 'empty.noneArchived': 'Aún no hay sesiones archivadas', + 'empty.viewOthers': 'Ver otras sesiones', + 'empty.noMatches': 'No hay coincidencias', + 'workspace.add': 'Agregar espacio de trabajo', + 'search.sessions.aria': 'Buscar sesiones', + 'search.placeholder': 'Buscar nombres de sesiones', + 'search.clear': 'Borrar búsqueda', + 'search.results.aria': 'Resultados de la búsqueda', + 'search.pending': 'Buscando el historial de sesiones…', + 'search.noMatches': 'No hay sesiones coincidentes', + 'search.hasMore': 'Mostrando los primeros resultados de {n}. Limita tu búsqueda.', + 'menu.addWorkspace': 'Agregar espacio de trabajo…', + 'picker.loading': 'Cargando espacios de trabajo…', + 'conflict.named': 'Ya existe un espacio de trabajo denominado "{name}".', + 'folderError.title': 'No se pudo abrir la carpeta', + 'folderError.retry': 'Elige de nuevo', + 'rename': 'Cambiar nombre', + 'rename.workspace.title': 'Cambiar nombre del espacio de trabajo', + 'rename.session.title': 'Cambiar nombre de sesión', + 'field.workspaceName': 'Nombre del espacio de trabajo', + 'field.sessionName': 'Nombre de la sesión', + 'delete.workspace': 'Eliminar espacio de trabajo', + 'delete.desc': 'Esto elimina "{name}" de la lista de espacios de trabajo. Se conservarán los registros de carpetas y sesiones. Sus sesiones aparecerán en Desagrupadas.', + 'delete.pending': 'Eliminando espacio de trabajo…', + 'menu.fork': 'Crear una bifurcación de la sesión', + 'menu.archiveSession': 'Sesión de archivo', + 'menu.unarchiveSession': 'Desarchivar sesión', + 'menu.pinSession': 'Fijar sesión', + 'menu.unpinSession': 'Desfijar sesión', + 'menu.sessionId': 'Copiar ID de sesión', + 'toast.sessionIdCopied': 'ID de sesión copiada', + 'toast.sessionIdCopyFailed': 'La copia falló. Intentar otra vez.', + 'row.archived': 'Archivado', + 'row.pinned': 'Fijado', + 'toast.archivedNotOpenable': 'Las sesiones archivadas no se pueden abrir. Desarchivarlo para verlo.', + 'toast.archived': 'Sesión archivada. tu puedes ', + 'toast.stoppedAndArchived': 'Sesión detenida y archivada. tu puedes ', + 'archive.confirm.title': '¿Detener y archivar esta sesión?', + 'archive.confirm.desc': '“{title}” todavía tiene trabajo en progreso. Archivarlo lo detiene primero; puedes restaurar la sesión más tarde desde el filtro “Todas las conversaciones (mostrar archivadas)” en la barra lateral, y el trabajo detenido no se reanudará por sí solo.', + 'archive.confirm.activity': 'Trabajo que será detenido', + 'archive.confirm.turn': 'El turno en curso', + 'archive.confirm.subagents.one': '{n} ejecutando subagente: {names}', + 'archive.confirm.subagents.other': '{n} ejecutando subagentes: {names}', + 'archive.confirm.jobs.one': 'Trabajo en segundo plano {n}: {names}', + 'archive.confirm.jobs.other': 'Trabajos en segundo plano {n}: {names}', + 'archive.confirm.schedules.one': 'Recordatorio programado {n}: {names}', + 'archive.confirm.schedules.other': '{n} recordatorios programados: {names}', + 'archive.confirm.other.one': '{n} otro elemento de trabajo ({kind})', + 'archive.confirm.other.other': '{n} otros elementos de trabajo ({kind})', + 'archive.confirm.listSeparator': ', ', + 'archive.confirm.action': 'Detener y archivar', + 'archive.confirm.pending': 'Deteniendo y archivando…', + 'toast.archivedUndo': 'deshacer', + 'toast.archivedOr': ' o ', + 'toast.archivedFilter': 'filtrar sesiones archivadas', + 'toast.pinFailed': 'No se pudo fijar. Inténtalo de nuevo más tarde.', + 'toast.unpinFailed': 'No se pudo desfijar. Inténtalo de nuevo más tarde.', + 'toast.createFailed': 'Nueva sesión falló: {message}', + 'toast.forkRequiresOpen': 'Abra la sesión original para completar la migración antes de crear una rama.', + 'toast.openForkSource': 'Abrir sesión original', + 'sessions.count.one': 'Sesión {n}', + 'sessions.count.other': 'Sesiones {n}', + 'actions.workspace.aria': 'Acciones del espacio de trabajo para {name}', + 'actions.session.aria': 'Acciones de sesión para {name}', + 'actions.archive': 'Archivo', + 'actions.unarchive': 'Desarchivar', + 'actions.pin': 'Fijar', + 'actions.unpin': 'Desfijar', + 'actions.newSession': 'Nueva sesión', + 'actions.newSession.aria': 'Nueva sesión en {name}', + 'status.running': 'corriendo', + 'status.subagentsRunning.one': 'Subagente {n} ejecutándose', + 'status.subagentsRunning.other': 'Subagentes {n} ejecutándose', + 'status.idle': 'inactivo', + 'status.waitingApproval': 'Esperando aprobación', + 'status.planReview': 'Plan pendiente de revisión', + 'status.waitingAnswer': 'esperando respuesta', + 'status.compact.approval': 'Aprobación', + 'status.compact.planReview': 'Revisión del plan', + 'status.compact.answer': 'respuesta', + 'status.completed': 'Completado', + 'hover.created': 'Creado {time}', + 'hover.copied': 'Copiado', + 'date.ymd': '{y}-{m}-{d}', + 'time.now': 'ahora', + 'time.minutes': '{n}min', + 'time.hours': '{n}h', + 'time.days': '{n}d', + 'time.months': '{n}mo', + 'time.years': '{n}y', + 'time.ago': 'Hace {t}', +} satisfies Record + +/** French dictionary, checked against the English keys. */ +export const fr = { + 'defaultWorkspace.failed': 'Impossible de créer un espace de travail par défaut. Utilisez Choisir un espace de travail pour sélectionner un dossier.', + 'draft.workspaceRequired': "Choisissez d'abord un espace de travail.", + 'draft.initializationFailed': 'Impossible de remplir le projet. Veuillez réessayer sous peu.', + 'group.ungrouped': 'Non groupé', + 'session.new': 'Nouvelle séance', + 'session.untitled': 'Sans titre', + 'shortcut.noSession': "Sélectionnez d'abord une session", + 'shortcut.noPicker': 'Sélecteur de répertoire indisponible', + 'shortcut.directoryBusy': "Sélection ou ajout d'un espace de travail", + 'shortcut.noCompletedTurn': "Cette session n'a pas de tour terminé", + 'shortcut.forkFailed': 'Impossible de bifurquer la session. Essayer à nouveau.', + 'section.workspaces': 'Espaces de travail', + 'section.sessions': 'Séances', + 'viewOptions.label': 'Afficher les options', + 'groupBy.label': 'Regrouper par', + 'groupBy.workspace': 'Espace de travail', + 'groupBy.workspaceTree': "Arborescence de l'espace de travail", + 'groupBy.flat': 'Dans une liste', + 'orderBy.label': 'Commander par', + 'orderBy.manual': 'Manuel', + 'orderBy.updated': 'Dernière mise à jour', + 'filterBy.label': 'Filtrer les sessions', + 'viewOptions.hideArchived': 'Masquer les archives', + 'viewOptions.showArchived': 'Toutes les conversations (afficher les archives)', + 'viewOptions.onlyArchived': 'Archivé uniquement', + 'sessions.expand': 'Afficher {n} plus de sessions', + 'sessions.collapse': 'Afficher moins', + 'empty.none': "Aucune séance pour l'instant", + 'empty.noneArchived': "Aucune session archivée pour l'instant", + 'empty.viewOthers': 'Voir les autres séances', + 'empty.noMatches': 'Aucune correspondance', + 'workspace.add': 'Ajouter un espace de travail', + 'search.sessions.aria': 'Rechercher des sessions', + 'search.placeholder': 'Rechercher des noms de sessions', + 'search.clear': 'Effacer la recherche', + 'search.results.aria': 'Résultats de la recherche', + 'search.pending': "Recherche dans l'historique des sessions…", + 'search.noMatches': 'Aucune session correspondante', + 'search.hasMore': 'Affichage des premiers résultats {n}. Affinez votre recherche.', + 'menu.addWorkspace': 'Ajouter un espace de travail…', + 'picker.loading': 'Chargement des espaces de travail…', + 'conflict.named': 'Un espace de travail nommé « {name} » existe déjà.', + 'folderError.title': "Impossible d'ouvrir le dossier", + 'folderError.retry': 'Choisir à nouveau', + 'rename': 'Renommer', + 'rename.workspace.title': "Renommer l'espace de travail", + 'rename.session.title': 'Renommer la session', + 'field.workspaceName': "Nom de l'espace de travail", + 'field.sessionName': 'Nom de la séance', + 'delete.workspace': "Supprimer l'espace de travail", + 'delete.desc': 'Cela supprime « {name} » de la liste des espaces de travail. Les journaux de dossiers et de sessions seront conservés. Ses sessions apparaîtront sous Non groupées.', + 'delete.pending': "Suppression de l'espace de travail…", + 'menu.fork': 'Créer une branche de la session', + 'menu.archiveSession': "Séance d'archivage", + 'menu.unarchiveSession': 'Session de désarchivage', + 'menu.pinSession': 'Épingler la session', + 'menu.unpinSession': 'Désépingler la session', + 'menu.sessionId': "Copier l'ID de session", + 'toast.sessionIdCopied': 'ID de session copié', + 'toast.sessionIdCopyFailed': 'La copie a échoué. Essayer à nouveau.', + 'row.archived': 'Archivé', + 'row.pinned': 'Épinglé', + 'toast.archivedNotOpenable': 'Les sessions archivées ne peuvent pas être ouvertes. Désarchivez-le pour le visualiser.', + 'toast.archived': 'Séance archivée. Vous pouvez ', + 'toast.stoppedAndArchived': 'Session arrêtée et archivée. Vous pouvez ', + 'archive.confirm.title': 'Arrêter et archiver cette session ?', + 'archive.confirm.desc': "« {title} » a encore des travaux en cours. L'archivage l'arrête en premier ; vous pouvez restaurer la session plus tard à partir du filtre « Toutes les conversations (afficher les archives) » dans la barre latérale, et le travail arrêté ne reprendra pas de lui-même.", + 'archive.confirm.activity': 'Travaux qui seront arrêtés', + 'archive.confirm.turn': 'Le tour en cours', + 'archive.confirm.subagents.one': "Sous-agent en cours d'exécution {n} : {names}", + 'archive.confirm.subagents.other': '{n} exécutant des sous-agents : {names}', + 'archive.confirm.jobs.one': 'Travail en arrière-plan {n} : {names}', + 'archive.confirm.jobs.other': 'Travaux en arrière-plan {n} : {names}', + 'archive.confirm.schedules.one': 'Rappel programmé {n} : {names}', + 'archive.confirm.schedules.other': 'Rappels programmés {n} : {names}', + 'archive.confirm.other.one': '{n} autre élément de travail ({kind})', + 'archive.confirm.other.other': '{n} autres éléments de travail ({kind})', + 'archive.confirm.listSeparator': ', ', + 'archive.confirm.action': 'Arrêter et archiver', + 'archive.confirm.pending': 'Arrêt et archivage…', + 'toast.archivedUndo': 'annuler', + 'toast.archivedOr': ' ou ', + 'toast.archivedFilter': 'filtrer les sessions archivées', + 'toast.pinFailed': 'L’épinglage a échoué. Réessayez plus tard.', + 'toast.unpinFailed': 'Impossible de désépingler. Réessayez plus tard.', + 'toast.createFailed': 'Échec de la nouvelle session : {message}', + 'toast.forkRequiresOpen': "Ouvrez la session d'origine pour terminer la migration avant de créer une branche.", + 'toast.openForkSource': 'Ouvrir la session originale', + 'sessions.count.one': 'Séance {n}', + 'sessions.count.other': 'Séances {n}', + 'actions.workspace.aria': "Actions de l'espace de travail pour {name}", + 'actions.session.aria': 'Actions de session pour {name}', + 'actions.archive': 'Archiver', + 'actions.unarchive': 'Désarchiver', + 'actions.pin': 'Épingler', + 'actions.unpin': 'Désépingler', + 'actions.newSession': 'Nouvelle séance', + 'actions.newSession.aria': 'Nouvelle séance dans {name}', + 'status.running': 'Courir', + 'status.subagentsRunning.one': "Sous-agent {n} en cours d'exécution", + 'status.subagentsRunning.other': "Sous-agents {n} en cours d'exécution", + 'status.idle': 'Inactif', + 'status.waitingApproval': "En attente d'approbation", + 'status.planReview': "Plan en attente d'examen", + 'status.waitingAnswer': 'En attente de réponse', + 'status.compact.approval': 'Approbation', + 'status.compact.planReview': 'Révision des plans', + 'status.compact.answer': 'Réponse', + 'status.completed': 'Terminé', + 'hover.created': 'Créé {time}', + 'hover.copied': 'Copié', + 'date.ymd': '{y}-{m}-{d}', + 'time.now': 'maintenant', + 'time.minutes': '{n}min', + 'time.hours': '{n}h', + 'time.days': '{n}d', + 'time.months': '{n}mo', + 'time.years': '{n}y', + 'time.ago': 'Il y a {t}', +} satisfies Record + +/** German dictionary, checked against the English keys. */ +export const de = { + 'defaultWorkspace.failed': 'Der Standardarbeitsbereich kann nicht erstellt werden. Verwenden Sie „Arbeitsbereich auswählen“, um einen Ordner auszuwählen.', + 'draft.workspaceRequired': 'Wählen Sie zunächst einen Arbeitsbereich.', + 'draft.initializationFailed': 'Der Entwurf konnte nicht ausgefüllt werden. Bitte versuchen Sie es in Kürze noch einmal.', + 'group.ungrouped': 'Nicht gruppiert', + 'session.new': 'Neue Sitzung', + 'session.untitled': 'Ohne Titel', + 'shortcut.noSession': 'Wählen Sie zunächst eine Sitzung aus', + 'shortcut.noPicker': 'Verzeichnisauswahl nicht verfügbar', + 'shortcut.directoryBusy': 'Auswählen oder Hinzufügen eines Arbeitsbereichs', + 'shortcut.noCompletedTurn': 'Diese Sitzung hat keine abgeschlossene Runde', + 'shortcut.forkFailed': 'Die Sitzung konnte nicht aufgeteilt werden. Versuchen Sie es erneut.', + 'section.workspaces': 'Arbeitsbereiche', + 'section.sessions': 'Sitzungen', + 'viewOptions.label': 'Optionen anzeigen', + 'groupBy.label': 'Gruppieren nach', + 'groupBy.workspace': 'WorkSpace', + 'groupBy.workspaceTree': 'Arbeitsbereichsbaum', + 'groupBy.flat': 'In einer Liste', + 'orderBy.label': 'Bestellen nach', + 'orderBy.manual': 'Handbuch', + 'orderBy.updated': 'Zuletzt aktualisiert', + 'filterBy.label': 'Sitzungen filtern', + 'viewOptions.hideArchived': 'Archiviert ausblenden', + 'viewOptions.showArchived': 'Alle Gespräche (archiviert anzeigen)', + 'viewOptions.onlyArchived': 'Nur archiviert', + 'sessions.expand': 'Weitere Sitzungen anzeigen {n}', + 'sessions.collapse': 'Weniger anzeigen', + 'empty.none': 'Noch keine Sitzungen', + 'empty.noneArchived': 'Noch keine archivierten Sitzungen', + 'empty.viewOthers': 'Andere Sitzungen anzeigen', + 'empty.noMatches': 'Keine Übereinstimmungen', + 'workspace.add': 'Arbeitsbereich hinzufügen', + 'search.sessions.aria': 'Sitzungen suchen', + 'search.placeholder': 'Suchen Sie nach Sitzungsnamen', + 'search.clear': 'Suche löschen', + 'search.results.aria': 'Suchergebnisse', + 'search.pending': 'Sitzungsverlauf wird durchsucht…', + 'search.noMatches': 'Keine passenden Sitzungen', + 'search.hasMore': 'Zeigt die ersten {n}-Ergebnisse. Grenzen Sie Ihre Suche ein.', + 'menu.addWorkspace': 'Arbeitsbereich hinzufügen…', + 'picker.loading': 'Arbeitsbereiche werden geladen…', + 'conflict.named': 'Ein Arbeitsbereich mit dem Namen „{name}“ ist bereits vorhanden.', + 'folderError.title': 'Der Ordner konnte nicht geöffnet werden', + 'folderError.retry': 'Wählen Sie erneut', + 'rename': 'Umbenennen', + 'rename.workspace.title': 'Arbeitsbereich umbenennen', + 'rename.session.title': 'Sitzung umbenennen', + 'field.workspaceName': 'Name des Arbeitsbereichs', + 'field.sessionName': 'Sitzungsname', + 'delete.workspace': 'Arbeitsbereich löschen', + 'delete.desc': 'Dadurch wird „{name}“ aus der Arbeitsbereichsliste entfernt. Die Ordner- und Sitzungsprotokolle werden aufbewahrt. Seine Sitzungen werden unter „Nicht gruppiert“ angezeigt.', + 'delete.pending': 'Arbeitsbereich wird gelöscht…', + 'menu.fork': 'Sitzung abzweigen', + 'menu.archiveSession': 'Archivsitzung', + 'menu.unarchiveSession': 'Sitzung dearchivieren', + 'menu.pinSession': 'Sitzung anheften', + 'menu.unpinSession': 'Sitzung lösen', + 'menu.sessionId': 'Sitzungs-ID kopieren', + 'toast.sessionIdCopied': 'Sitzungs-ID kopiert', + 'toast.sessionIdCopyFailed': 'Der Kopiervorgang ist fehlgeschlagen. Versuchen Sie es erneut.', + 'row.archived': 'Archiviert', + 'row.pinned': 'Angepinnt', + 'toast.archivedNotOpenable': 'Archivierte Sitzungen können nicht geöffnet werden. Entpacken Sie es zur Ansicht.', + 'toast.archived': 'Sitzung archiviert. Du kannst ', + 'toast.stoppedAndArchived': 'Sitzung gestoppt und archiviert. Du kannst ', + 'archive.confirm.title': 'Diese Sitzung stoppen und archivieren?', + 'archive.confirm.desc': '„{title}“ ist noch in Arbeit. Die Archivierung stoppt es zunächst; Sie können die Sitzung später über den Filter „Alle Konversationen (archivierte anzeigen)“ in der Seitenleiste wiederherstellen, und die angehaltene Arbeit wird nicht von alleine fortgesetzt.', + 'archive.confirm.activity': 'Arbeiten, die gestoppt werden', + 'archive.confirm.turn': 'Die laufende Runde', + 'archive.confirm.subagents.one': '{n}, laufender Subagent: {names}', + 'archive.confirm.subagents.other': '{n}, auf dem Subagenten ausgeführt werden: {names}', + 'archive.confirm.jobs.one': '{n} Hintergrundjob: {names}', + 'archive.confirm.jobs.other': '{n} Hintergrundjobs: {names}', + 'archive.confirm.schedules.one': '{n} geplante Erinnerung: {names}', + 'archive.confirm.schedules.other': '{n} geplante Erinnerungen: {names}', + 'archive.confirm.other.one': '{n} sonstiger Arbeitsgegenstand ({kind})', + 'archive.confirm.other.other': '{n} sonstige Arbeiten ({kind})', + 'archive.confirm.listSeparator': ', ', + 'archive.confirm.action': 'Stoppen und archivieren', + 'archive.confirm.pending': 'Stoppen und archivieren…', + 'toast.archivedUndo': 'rückgängig machen', + 'toast.archivedOr': ' oder ', + 'toast.archivedFilter': 'Archivierte Sitzungen filtern', + 'toast.pinFailed': 'Anheften fehlgeschlagen. Versuchen Sie es später erneut.', + 'toast.unpinFailed': 'Lösen fehlgeschlagen. Versuchen Sie es später erneut.', + 'toast.createFailed': 'Neue Sitzung fehlgeschlagen: {message}', + 'toast.forkRequiresOpen': 'Öffnen Sie die ursprüngliche Sitzung, um die Migration abzuschließen, bevor Sie einen Zweig erstellen.', + 'toast.openForkSource': 'Öffnen Sie die ursprüngliche Sitzung', + 'sessions.count.one': '{n}-Sitzung', + 'sessions.count.other': '{n}-Sitzungen', + 'actions.workspace.aria': 'Arbeitsbereichsaktionen für {name}', + 'actions.session.aria': 'Sitzungsaktionen für {name}', + 'actions.archive': 'Archiv', + 'actions.unarchive': 'Dearchivieren', + 'actions.pin': 'Anheften', + 'actions.unpin': 'Lösen', + 'actions.newSession': 'Neue Sitzung', + 'actions.newSession.aria': 'Neue Sitzung in {name}', + 'status.running': 'Laufen', + 'status.subagentsRunning.one': '{n}-Subagent läuft', + 'status.subagentsRunning.other': '{n} Subagenten laufen', + 'status.idle': 'Leerlauf', + 'status.waitingApproval': 'Warten auf Genehmigung', + 'status.planReview': 'Plan wartet auf Überprüfung', + 'status.waitingAnswer': 'Warte auf Antwort', + 'status.compact.approval': 'Zustimmung', + 'status.compact.planReview': 'Planüberprüfung', + 'status.compact.answer': 'Antwort', + 'status.completed': 'Abgeschlossen', + 'hover.created': 'Erstellt {time}', + 'hover.copied': 'Kopiert', + 'date.ymd': '{y}-{m}-{d}', + 'time.now': 'jetzt', + 'time.minutes': '{n}min', + 'time.hours': '{n}h', + 'time.days': '{n}d', + 'time.months': '{n}mo', + 'time.years': '{n}y', + 'time.ago': 'Vor {t}', +} satisfies Record + +/** Russian dictionary, checked against the English keys. */ +export const ru = { + 'defaultWorkspace.failed': 'Невозможно создать рабочую область по умолчанию. Используйте «Выбрать рабочее пространство», чтобы выбрать папку.', + 'draft.workspaceRequired': 'Сначала выберите рабочее место.', + 'draft.initializationFailed': 'Не удалось заполнить черновик. Пожалуйста, повторите попытку в ближайшее время.', + 'group.ungrouped': 'Разгруппировано', + 'session.new': 'Новая сессия', + 'session.untitled': 'Без названия', + 'shortcut.noSession': 'Сначала выберите сеанс', + 'shortcut.noPicker': 'Выбор каталога недоступен.', + 'shortcut.directoryBusy': 'Выбор или добавление рабочего пространства', + 'shortcut.noCompletedTurn': 'В этой сессии нет завершенных ходов', + 'shortcut.forkFailed': 'Не удалось разветвить сеанс. Попробуйте еще раз.', + 'section.workspaces': 'Рабочие пространства', + 'section.sessions': 'Сессии', + 'viewOptions.label': 'Посмотреть варианты', + 'groupBy.label': 'Группировать по', + 'groupBy.workspace': 'Рабочее пространство', + 'groupBy.workspaceTree': 'Дерево рабочей области', + 'groupBy.flat': 'В одном списке', + 'orderBy.label': 'Заказать по', + 'orderBy.manual': 'Руководство', + 'orderBy.updated': 'Последнее обновление', + 'filterBy.label': 'Фильтровать сеансы', + 'viewOptions.hideArchived': 'Скрыть в архиве', + 'viewOptions.showArchived': 'Все разговоры (показать в архиве)', + 'viewOptions.onlyArchived': 'Только в архиве', + 'sessions.expand': 'Показать больше сеансов {n}', + 'sessions.collapse': 'Показать меньше', + 'empty.none': 'Сеансов пока нет', + 'empty.noneArchived': 'Архивированных сессий пока нет', + 'empty.viewOthers': 'Посмотреть другие сессии', + 'empty.noMatches': 'Нет совпадений', + 'workspace.add': 'Добавить рабочую область', + 'search.sessions.aria': 'Поиск сеансов', + 'search.placeholder': 'Поиск названий сеансов', + 'search.clear': 'Очистить поиск', + 'search.results.aria': 'Результаты поиска', + 'search.pending': 'Поиск истории сеансов…', + 'search.noMatches': 'Нет подходящих сеансов', + 'search.hasMore': 'Показаны первые результаты {n}. Сузьте область поиска.', + 'menu.addWorkspace': 'Добавить рабочее пространство…', + 'picker.loading': 'Загрузка рабочих областей…', + 'conflict.named': 'Рабочая область с именем «{name}» уже существует.', + 'folderError.title': 'Не удалось открыть папку', + 'folderError.retry': 'Выбрать еще раз', + 'rename': 'Переименовать', + 'rename.workspace.title': 'Переименовать рабочую область', + 'rename.session.title': 'Переименовать сеанс', + 'field.workspaceName': 'Имя рабочей области', + 'field.sessionName': 'Имя сеанса', + 'delete.workspace': 'Удалить рабочую область', + 'delete.desc': 'Это удалит «{name}» из списка рабочей области. Журналы папок и сеансов будут сохранены. Его сеансы появятся в разделе «Разгруппировано».', + 'delete.pending': 'Удаление рабочей области…', + 'menu.fork': 'Создать ответвление сеанса', + 'menu.archiveSession': 'Архив сеанса', + 'menu.unarchiveSession': 'Разархивировать сеанс', + 'menu.pinSession': 'Закрепить сеанс', + 'menu.unpinSession': 'Открепить сеанс', + 'menu.sessionId': 'Копировать идентификатор сеанса', + 'toast.sessionIdCopied': 'Идентификатор сеанса скопирован.', + 'toast.sessionIdCopyFailed': 'Копировать не удалось. Попробуйте еще раз.', + 'row.archived': 'В архиве', + 'row.pinned': 'Закреплено', + 'toast.archivedNotOpenable': 'Архивированные сеансы невозможно открыть. Разархивируйте его для просмотра.', + 'toast.archived': 'Сессия заархивирована. Вы можете ', + 'toast.stoppedAndArchived': 'Сеанс остановлен и заархивирован. Вы можете ', + 'archive.confirm.title': 'Остановить и заархивировать этот сеанс?', + 'archive.confirm.desc': 'Над «{title}» все еще ведется работа. Архивирование останавливает его первым; восстановить сессию можно позже из фильтра «Все разговоры (показать в архиве)» в боковой панели, при этом остановленная работа не возобновится сама по себе.', + 'archive.confirm.activity': 'Работа, которая будет остановлена', + 'archive.confirm.turn': 'Текущий ход', + 'archive.confirm.subagents.one': '{n} работающий субагент: {names}', + 'archive.confirm.subagents.other': '{n} работающие субагенты: {names}', + 'archive.confirm.jobs.one': '{n} фоновое задание: {names}', + 'archive.confirm.jobs.other': '{n} фоновые задания: {names}', + 'archive.confirm.schedules.one': 'Запланированное напоминание {n}: {names}', + 'archive.confirm.schedules.other': 'Запланированные напоминания {n}: {names}', + 'archive.confirm.other.one': '{n} другой элемент работы ({kind})', + 'archive.confirm.other.other': '{n} прочие работы ({kind})', + 'archive.confirm.listSeparator': ', ', + 'archive.confirm.action': 'Остановить и заархивировать', + 'archive.confirm.pending': 'Остановка и архивирование…', + 'toast.archivedUndo': 'отменить', + 'toast.archivedOr': ' или ', + 'toast.archivedFilter': 'фильтровать архивированные сеансы', + 'toast.pinFailed': 'Не удалось закрепить. Повторите попытку позже.', + 'toast.unpinFailed': 'Не удалось открепить. Повторите попытку позже.', + 'toast.createFailed': 'Не удалось создать новый сеанс: {message}.', + 'toast.forkRequiresOpen': 'Откройте исходный сеанс, чтобы завершить миграцию, прежде чем создавать ветку.', + 'toast.openForkSource': 'Открыть исходный сеанс', + 'sessions.count.one': 'Сеанс {n}', + 'sessions.count.other': 'Сессии {n}', + 'actions.workspace.aria': 'Действия рабочей области для {name}', + 'actions.session.aria': 'Действия сеанса для {name}', + 'actions.archive': 'Архив', + 'actions.unarchive': 'Разархивировать', + 'actions.pin': 'Закрепить', + 'actions.unpin': 'Открепить', + 'actions.newSession': 'Новая сессия', + 'actions.newSession.aria': 'Новая сессия в {name}', + 'status.running': 'Бег', + 'status.subagentsRunning.one': 'Субагент {n} работает', + 'status.subagentsRunning.other': 'Субагенты {n} работают', + 'status.idle': 'Простой', + 'status.waitingApproval': 'Ожидание одобрения', + 'status.planReview': 'План ожидает рассмотрения', + 'status.waitingAnswer': 'Ожидание ответа', + 'status.compact.approval': 'Одобрение', + 'status.compact.planReview': 'Обзор плана', + 'status.compact.answer': 'Ответ', + 'status.completed': 'Завершено', + 'hover.created': 'Создано {time}', + 'hover.copied': 'Скопировано', + 'date.ymd': '{y}-{m}-{d}', + 'time.now': 'сейчас', + 'time.minutes': '{n}мин', + 'time.hours': '{n}h', + 'time.days': '{n}d', + 'time.months': '{n}mo', + 'time.years': '{n}y', + 'time.ago': '{t} назад', +} satisfies Record + +/** Portuguese dictionary, checked against the English keys. */ +export const pt = { + 'defaultWorkspace.failed': 'Não foi possível criar o espaço de trabalho padrão. Use Escolher espaço de trabalho para selecionar uma pasta.', + 'draft.workspaceRequired': 'Escolha um espaço de trabalho primeiro.', + 'draft.initializationFailed': 'Não foi possível preencher o rascunho. Por favor, tente novamente em breve.', + 'group.ungrouped': 'Desagrupado', + 'session.new': 'Nova sessão', + 'session.untitled': 'Sem título', + 'shortcut.noSession': 'Selecione uma sessão primeiro', + 'shortcut.noPicker': 'Seletor de diretório indisponível', + 'shortcut.directoryBusy': 'Selecionando ou adicionando um espaço de trabalho', + 'shortcut.noCompletedTurn': 'Esta sessão não tem turno concluído', + 'shortcut.forkFailed': 'Não foi possível bifurcar a sessão. Tente novamente.', + 'section.workspaces': 'Espaços de trabalho', + 'section.sessions': 'Sessões', + 'viewOptions.label': 'Ver opções', + 'groupBy.label': 'Agrupar por', + 'groupBy.workspace': 'Espaço de trabalho', + 'groupBy.workspaceTree': 'Árvore do espaço de trabalho', + 'groupBy.flat': 'Em uma lista', + 'orderBy.label': 'Encomendar por', + 'orderBy.manual': 'Manuais', + 'orderBy.updated': 'Última atualização', + 'filterBy.label': 'Filtrar sessões', + 'viewOptions.hideArchived': 'Ocultar arquivado', + 'viewOptions.showArchived': 'Todas as conversas (mostrar arquivadas)', + 'viewOptions.onlyArchived': 'Somente arquivado', + 'sessions.expand': 'Mostrar mais {n} sessões', + 'sessions.collapse': 'Mostrar menos', + 'empty.none': 'Ainda não há sessões', + 'empty.noneArchived': 'Ainda não há sessões arquivadas', + 'empty.viewOthers': 'Ver outras sessões', + 'empty.noMatches': 'Nenhuma correspondência', + 'workspace.add': 'Adicionar espaço de trabalho', + 'search.sessions.aria': 'Buscar sessões', + 'search.placeholder': 'Pesquisar nomes de sessões', + 'search.clear': 'Limpar pesquisa', + 'search.results.aria': 'Resultados da pesquisa', + 'search.pending': 'Pesquisando histórico da sessão…', + 'search.noMatches': 'Nenhuma sessão correspondente', + 'search.hasMore': 'Mostrando os primeiros {n} resultados. Restrinja sua pesquisa.', + 'menu.addWorkspace': 'Adicionar espaço de trabalho…', + 'picker.loading': 'Carregando áreas de trabalho…', + 'conflict.named': 'Já existe um espaço de trabalho chamado “{name}”.', + 'folderError.title': 'Não foi possível abrir a pasta', + 'folderError.retry': 'Escolha novamente', + 'rename': 'Renomear', + 'rename.workspace.title': 'Renomear espaço de trabalho', + 'rename.session.title': 'Renomear sessão', + 'field.workspaceName': 'Nome do espaço de trabalho', + 'field.sessionName': 'Nome da sessão', + 'delete.workspace': 'Excluir espaço de trabalho', + 'delete.desc': 'Isso remove “{name}” da lista de espaços de trabalho. A pasta e os logs de sessão serão mantidos. Suas sessões aparecerão em Desagrupado.', + 'delete.pending': 'Excluindo espaço de trabalho…', + 'menu.fork': 'Bifurcar sessão', + 'menu.archiveSession': 'Sessão de arquivamento', + 'menu.unarchiveSession': 'Desarquivar sessão', + 'menu.pinSession': 'Fixar sessão', + 'menu.unpinSession': 'Desafixar sessão', + 'menu.sessionId': 'Copiar ID da sessão', + 'toast.sessionIdCopied': 'ID da sessão copiado', + 'toast.sessionIdCopyFailed': 'Falha na cópia. Tente novamente.', + 'row.archived': 'Arquivado', + 'row.pinned': 'Fixado', + 'toast.archivedNotOpenable': 'As sessões arquivadas não podem ser abertas. Desarquive-o para visualizar.', + 'toast.archived': 'Sessão arquivada. Você pode ', + 'toast.stoppedAndArchived': 'Sessão interrompida e arquivada. Você pode ', + 'archive.confirm.title': 'Parar e arquivar esta sessão?', + 'archive.confirm.desc': '“{title}” ainda tem trabalho em andamento. O arquivamento interrompe primeiro; você pode restaurar a sessão posteriormente a partir do filtro “Todas as conversas (mostrar arquivadas)” na barra lateral, e o trabalho interrompido não será retomado por conta própria.', + 'archive.confirm.activity': 'Obra que será interrompida', + 'archive.confirm.turn': 'O turno em andamento', + 'archive.confirm.subagents.one': '{n} subagente em execução: {names}', + 'archive.confirm.subagents.other': '{n} executando subagentes: {names}', + 'archive.confirm.jobs.one': '{n} trabalho em segundo plano: {names}', + 'archive.confirm.jobs.other': '{n} trabalhos em segundo plano: {names}', + 'archive.confirm.schedules.one': '{n} lembrete programado: {names}', + 'archive.confirm.schedules.other': '{n} lembretes programados: {names}', + 'archive.confirm.other.one': '{n} outro item de trabalho ({kind})', + 'archive.confirm.other.other': '{n} outros itens de trabalho ({kind})', + 'archive.confirm.listSeparator': ', ', + 'archive.confirm.action': 'Pare e arquive', + 'archive.confirm.pending': 'Parando e arquivando…', + 'toast.archivedUndo': 'desfazer', + 'toast.archivedOr': ' ou ', + 'toast.archivedFilter': 'filtrar sessões arquivadas', + 'toast.pinFailed': 'Não foi possível fixar. Tente novamente mais tarde.', + 'toast.unpinFailed': 'Não foi possível desafixar. Tente novamente mais tarde.', + 'toast.createFailed': 'Falha na nova sessão: {message}', + 'toast.forkRequiresOpen': 'Abra a sessão original para concluir a migração antes de criar uma ramificação.', + 'toast.openForkSource': 'Abrir sessão original', + 'sessions.count.one': '{n} sessão', + 'sessions.count.other': '{n} sessões', + 'actions.workspace.aria': 'Ações do espaço de trabalho para {name}', + 'actions.session.aria': 'Ações de sessão para {name}', + 'actions.archive': 'Arquivo', + 'actions.unarchive': 'Desarquivar', + 'actions.pin': 'Fixar', + 'actions.unpin': 'Desafixar', + 'actions.newSession': 'Nova sessão', + 'actions.newSession.aria': 'Nova sessão em {name}', + 'status.running': 'Correndo', + 'status.subagentsRunning.one': '{n} subagente em execução', + 'status.subagentsRunning.other': '{n} subagentes em execução', + 'status.idle': 'Inativo', + 'status.waitingApproval': 'Aguardando aprovação', + 'status.planReview': 'Plano aguardando revisão', + 'status.waitingAnswer': 'Esperando por resposta', + 'status.compact.approval': 'Aprovação', + 'status.compact.planReview': 'Revisão do plano', + 'status.compact.answer': 'Resposta', + 'status.completed': 'Concluído', + 'hover.created': 'Criado {time}', + 'hover.copied': 'Copiado', + 'date.ymd': '{y}-{m}-{d}', + 'time.now': 'agora', + 'time.minutes': '{n}min', + 'time.hours': '{n}h', + 'time.days': '{n}d', + 'time.months': '{n} mês', + 'time.years': '{n}s', + 'time.ago': '{t} atrás', +} satisfies Record diff --git a/packages/client/ui-workspace/src/client/navigation.ts b/packages/client/ui-workspace/src/client/navigation.ts index 6b4221ae19..42265f1af6 100644 --- a/packages/client/ui-workspace/src/client/navigation.ts +++ b/packages/client/ui-workspace/src/client/navigation.ts @@ -71,9 +71,10 @@ export interface UiWorkspace { options?: Pick[0], 'allowMigration'>, ): Promise /** - * Resolve the Session for a new conversation in a Workspace: navigation names the first - * reusable blank member, skipping blanks that list a preset other than a requested one, and - * the Host reuses it under the requested preset or its default, or creates another Session. + * Resolve the Session for a new conversation in a Workspace: navigation names the first reusable + * blank member — listed in the Workspace, not archived, and listing no preset or the requested one — + * and the Host reuses it under the requested preset or its default, creating another Session when it + * reuses none. Concurrent calls for the same Workspace and preset share one attempt. * @param workspaceId - target Workspace. * @param options - choices for the created or reused Session. * @returns a Session already addressable through the Session Controller. diff --git a/packages/client/ui-workspace/src/client/rows/Rows.tsx b/packages/client/ui-workspace/src/client/rows/Rows.tsx index 291c3f7ff4..d34f1b4a59 100644 --- a/packages/client/ui-workspace/src/client/rows/Rows.tsx +++ b/packages/client/ui-workspace/src/client/rows/Rows.tsx @@ -23,6 +23,7 @@ import { import type { ShortcutCatalogEntry } from '@deepseek-ai/dsh-client-shortcuts/client' import type { StateDotState } from '@deepseek-ai/dsh-client-ui-primitives' import { abbreviateHomePath } from '@deepseek-ai/dsh-util-workspace-path' +import type { WorkspaceId } from '@deepseek-ai/dsh-api-workspace-controller/client' import type { MenuOpenState, WorkspaceBrowserProps } from '../contract/slots.ts' import type { GroupNode, SearchResultNode, SessionNode } from '../tree.ts' import css from './Rows.module.css' @@ -203,21 +204,8 @@ function rowHalf(e: { clientY: number; currentTarget: HTMLElement }): 'before' | return e.clientY < rect.top + rect.height / 2 ? 'before' : 'after' } -/** - * Project (workspace) header row: folder + title; - * hover reveals the chevron and create button, and dwelling on a real - * Workspace shows its hover card (the ungrouped bucket has none). - * `containsCurrent` arrives on the node (derivation fact, no renderer scan). - * @param props.group - derived group node. - * @param props.containsCurrentDescendant - highlight an ancestor even when its subtree is collapsed. - * @param props.onToggle - expand/collapse the group. - * @param props.onCreate - start a frontend Session inside this Workspace. - * @param props.drag - optional workspace-row drag wiring. - * @param props.home - host account home for POSIX hover-path abbreviation. - * @param props.t - the browser root's locale seat. - * @returns the row element. - */ -export function ProjectRowItem({ group, containsCurrentDescendant = false, onToggle, onCreate, actions, drag, home, newShortcut, t }: { +/** Owner props of one Workspace header row; the access Hooks are the browser root's own standard seats. */ +type ProjectRowItemProps = { group: GroupNode newShortcut?: ShortcutCatalogEntry | undefined containsCurrentDescendant?: boolean @@ -230,7 +218,62 @@ export function ProjectRowItem({ group, containsCurrentDescendant = false, onTog /** Host account home; POSIX home-rooted hover paths display as `~`. */ home?: string | undefined t: RowTranslate -}) { +} & Pick + +/** Which of one header row's actions the caller's access snapshot allows. */ +interface ProjectRowAccess { + readonly create: boolean + readonly rename: boolean + readonly delete: boolean + readonly reorder: boolean +} + +/** + * Project (workspace) header row: folder + title; + * hover reveals the chevron and create button, and dwelling on a real + * Workspace shows its hover card (the ungrouped bucket has none). + * `containsCurrent` arrives on the node (derivation fact, no renderer scan). + * The create button, each Workspace menu row, and the drag handle are absent + * while the access snapshot excludes their endpoint: a real Workspace asks + * its own resource, and the ungrouped bucket asks the global resource. + * @param props.group - derived group node. + * @param props.containsCurrentDescendant - highlight an ancestor even when its subtree is collapsed. + * @param props.onToggle - expand/collapse the group. + * @param props.onCreate - start a frontend Session inside this Workspace. + * @param props.drag - optional workspace-row drag wiring. + * @param props.home - host account home for POSIX hover-path abbreviation. + * @param props.t - the browser root's locale seat. + * @returns the row element. + */ +export function ProjectRowItem(props: ProjectRowItemProps) { + const workspaceId = props.group.workspaceId + return workspaceId === undefined + ? + : +} + +/** The ungrouped bucket's header: no Workspace resource, so starting a Session there asks the global resource. */ +function UngroupedProjectRow(props: ProjectRowItemProps) { + const create = props.useRemoteAccess('session/create') + return +} + +/** A real Workspace's header: every action asks that Workspace's resource. */ +function WorkspaceProjectRow({ workspaceId, ...props }: ProjectRowItemProps & { workspaceId: WorkspaceId }) { + const { useWorkspaceAccess } = props + const access: ProjectRowAccess = { + create: useWorkspaceAccess(workspaceId, 'session/create'), + rename: useWorkspaceAccess(workspaceId, 'workspace/rename'), + delete: useWorkspaceAccess(workspaceId, 'workspace/delete'), + reorder: useWorkspaceAccess(workspaceId, 'workspace/insertBefore'), + } + return +} + +function ProjectRow({ + group, containsCurrentDescendant = false, onToggle, onCreate, actions, drag: offeredDrag, home, newShortcut, t, access, +}: ProjectRowItemProps & { access: ProjectRowAccess }) { + const drag = access.reorder ? offeredDrag : undefined const row = group // The ungrouped bucket has no workspace title: its label is dictionary copy. const label = row.workspaceId === undefined ? t('group.ungrouped') : row.label @@ -238,8 +281,8 @@ export function ProjectRowItem({ group, containsCurrentDescendant = false, onTog const [menuOpen, setMenuOpen] = useState(false) const dragPreview = useRowDragPreview(css.dragPreview as string) const workspaceMenuItems = [ - { id: 'rename', label: t('rename'), icon: }, - { id: 'delete', label: t('delete.workspace'), icon: , danger: true }, + ...access.rename ? [{ id: 'rename', label: t('rename'), icon: }] : [], + ...access.delete ? [{ id: 'delete', label: t('delete.workspace'), icon: , danger: true }] : [], ] const ownRow = (
{label} - {actions !== undefined && ( + {actions !== undefined && workspaceMenuItems.length > 0 && ( { setMenuOpen(false) }} @@ -297,17 +340,19 @@ export function ProjectRowItem({ group, containsCurrentDescendant = false, onTog )} /> )} - - - + {access.create && ( + + + + )}
) @@ -472,17 +517,22 @@ function SessionHoverContent({ node, cwd, now, renderSlot, t }: { * @param props.currentId - selected session id. * @param props.onOpen - open the selected session. * @param props.onUnarchive - unarchive an archived result row. + * @param props.useSessionAccess - the browser root's Session access Hook; the restore control + * appears only while the snapshot allows `workspace/unarchiveSession` for that Session. * @param props.t - Workspace-browser translation seat. * @returns the result row. */ -export function SearchResultItem({ result, currentId, onOpen, onUnarchive, t }: { +export function SearchResultItem({ + result, currentId, onOpen, onUnarchive, useSessionAccess, t, +}: { result: SearchResultNode currentId: string | undefined onOpen: (id: SearchResultNode['id']) => void onUnarchive: (id: SearchResultNode['id']) => void t: RowTranslate -}) { +} & Pick) { const selected = result.id === currentId + const canUnarchive = useSessionAccess(result.id, 'workspace/unarchiveSession') const statuses = sessionStatuses(result, t) const primaryStatus = statuses[0] return ( @@ -503,7 +553,7 @@ export function SearchResultItem({ result, currentId, onOpen, onUnarchive, t }: )} {result.title || t('session.untitled')} - {result.archived && ( + {result.archived && canUnarchive && (