From b31a9a601ff09b0cb0696b9b7e6bc62d27ffc1ee Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Thu, 13 Aug 2026 10:26:57 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20md=20=E9=A2=84=E8=A7=88=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=9D=97=E5=A4=8D=E5=88=B6=E6=8C=89=E9=92=AE=E8=B7=9F?= =?UTF-8?q?=E9=9A=8F=20DSH=20i18n=EF=BC=88MarkdownText=20=E8=A1=A5?= =?UTF-8?q?=E4=BC=A0=20codeLabels=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 +- .../2026-08-12-follow-dsh-i18n-design.md | 5 ++ src/client/TextEditor.tsx | 10 ++- tests/markdown-copy-labels.spec.tsx | 73 +++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 tests/markdown-copy-labels.spec.tsx diff --git a/AGENTS.md b/AGENTS.md index 7826016..9220cc3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -407,7 +407,7 @@ interface BetterSidebarService { | **host 半无此服务** | `ctx.betterSidebar` 只在 client 侧存在;host 半需要 better-sidebar 数据走 `/sidebar/api/*` HTTP 路由 | | **portal 限制** | 整面板 slot 由 ui-layout 独占,外部 tab 只能进入 better-sidebar 的 portal 内部,无法全屏替换 | | **id 冲突** | `registerTab` / `registerFileViewer` 对重复 id 抛错;建议用包前缀(`my-plugin:xxx`) | -| **i18n 跟随** | 侧边栏界面文案跟随 DSH 的 `ctx.locale`(`@deepseek-ai/dsh-client-locale`):词典注册在 `betterSidebar` 命名空间,语言偏好(Host-backed `locale.preference`)与浏览器语言不一致时以 DSH 为准并实时切换;locale 服务缺失时回退浏览器语言。插件自身的 `t()`(`src/client/locales.ts`)由 `apply()` 挂接服务;消费插件**不要**依赖此内部函数——标题等字段传字符串或 `() => string` 即可(i18n 友好) | +| **i18n 跟随** | 侧边栏界面文案跟随 DSH 的 `ctx.locale`(`@deepseek-ai/dsh-client-locale`):词典注册在 `betterSidebar` 命名空间,语言偏好(Host-backed `locale.preference`)与浏览器语言不一致时以 DSH 为准并实时切换;locale 服务缺失时回退浏览器语言。插件自身的 `t()`(`src/client/locales.ts`)由 `apply()` 挂接服务;消费插件**不要**依赖此内部函数——标题等字段传字符串或 `() => string` 即可(i18n 友好)。⚠️ 渲染 DSH 的 `MarkdownText` 时必须传 `codeLabels={{ copyLabel: t('copy'), copiedLabel: t('copied') }}`——该组件 cordis-free,漏传则代码块复制按钮回退硬编码中文 | | **懒加载 chunk** | 内置重依赖(Univer/xterm/CodeMirror/pptx/docx)在独立 bundle(`lib/client-.js`)中,经 `/sidebar/bundle` 路由按需下发;每个脚本把 factory 赋到插件自有全局注册表 `globalThis.__dshChunks__[]`,由 `src/client/chunk-loader.ts` 用自定义 require(externals 经 `__DSH_MODULES__` seed 分支解析)物化——**不经过** `__ModuleLoader__` 注册;**核心 bundle 禁止静态 import `src/client/chunks/*`**(会把库拖回启动路径);对消费插件透明——懒加载只作用于内置 descriptor,`component` 契约(`(props) => ReactNode` 纯渲染函数)不变 | --- diff --git a/docs/plans/2026-08-12-follow-dsh-i18n-design.md b/docs/plans/2026-08-12-follow-dsh-i18n-design.md index ef55f5a..dfafe84 100644 --- a/docs/plans/2026-08-12-follow-dsh-i18n-design.md +++ b/docs/plans/2026-08-12-follow-dsh-i18n-design.md @@ -50,6 +50,11 @@ DSH 的 i18n 系统(`@deepseek-ai/dsh-client-locale`): - 无。计划与实施一致。 - 版本号随 v0.8.2 发版提交(`package.json` + `dsh.plugin.json`)。 +- v0.8.2 后补遗:md 预览(`TextEditor.tsx`)的代码块复制按钮文案——DSH 的 + `MarkdownText`/`CodeBlock` 是 cordis-free 组件,未传 `codeLabels` 时回退到硬编码中文 + 「复制/复制成功」。修复为渲染时传入 `codeLabels={{ copyLabel: t('copy'), copiedLabel: t('copied') }}` + (对齐 DSH 聊天区 `AssistantMarkdown` 的做法),新增 `tests/markdown-copy-labels.spec.tsx` + 钉住 zh/en 跟随。 ## 验收 diff --git a/src/client/TextEditor.tsx b/src/client/TextEditor.tsx index 9035599..5594e35 100644 --- a/src/client/TextEditor.tsx +++ b/src/client/TextEditor.tsx @@ -329,7 +329,15 @@ export function TextEditor(props: FileViewerProps) { onMouseUp={handlePreviewMouseUp} onScroll={hidePopup} > - + {/* The fence copy-button labels must come from this plugin's own + dictionary: the DSH MarkdownText/CodeBlock are cordis-free and + fall back to hardcoded Chinese otherwise (same pattern as the + chat's AssistantMarkdown). Render-time t() keeps them following + the active locale on live switches. */} + )} {html && mode === 'preview' && ( diff --git a/tests/markdown-copy-labels.spec.tsx b/tests/markdown-copy-labels.spec.tsx new file mode 100644 index 0000000..fc57ddb --- /dev/null +++ b/tests/markdown-copy-labels.spec.tsx @@ -0,0 +1,73 @@ +/** + * Markdown preview copy-label spec: the fence copy buttons ("复制" / "Copy") + * must follow the DSH locale service through `codeLabels` — the DSH + * MarkdownText/CodeBlock are cordis-free and fall back to HARDCODED Chinese + * when the caller omits the labels, so TextEditor must pass its own + * dictionary copy (`t('copy')` / `t('copied')`), re-evaluated per render. + * Rendered with renderToString (the initial preview mode never mounts + * CodeMirror, so no DOM/effects are needed). + */ +import { describe, expect, it, afterEach } from 'vitest' +import { renderToString } from 'react-dom/server' +import { createElement } from 'react' +import './browser-globals.ts' +import { TextEditor } from '../src/client/TextEditor.tsx' +import { createSidebarStore } from '../src/client/state.ts' +import { attachLocale } from '../src/client/locales.ts' +import type { FileViewerProps } from '../src/client/service.ts' + +/** Minimal structural fake of the DSH LocaleService face the sidebar uses. */ +class FakeLocale { + active: string = 'zh' + getSnapshot(): { active: string } { + return { active: this.active } + } + subscribe(_fn: () => void): () => void { + return () => {} + } + register(_ns: string, _locale: string, _dict: Record): () => void { + return () => {} + } +} + +const CTX = {} as Parameters[0]['ctx'] + +/** A markdown source with one fenced code block (the copy-button surface). */ +const MD_WITH_FENCE = '```ts\nconst a = 1\n```' + +function viewerProps(overrides: Partial = {}): FileViewerProps { + return { + ctx: CTX, + store: createSidebarStore(), + scope: { sessionId: 's1', cwd: '/p' }, + path: '/p/a/README.md', + title: 'README.md', + viewerId: 'markdown', + content: MD_WITH_FENCE, + ...overrides, + } +} + +afterEach(() => { + attachLocale(undefined) +}) + +describe('markdown preview code-block copy labels (DSH i18n following)', () => { + it('renders the fence copy button with the zh dictionary label by default', () => { + const locale = new FakeLocale() + locale.active = 'zh' + attachLocale(locale) + const html = renderToString(createElement(TextEditor, viewerProps())) + expect(html).toContain('复制') + expect(html).not.toContain('Copy') + }) + + it('follows the attached locale service live: en renders "Copy" instead of the zh label', () => { + const locale = new FakeLocale() + locale.active = 'en' + attachLocale(locale) + const html = renderToString(createElement(TextEditor, viewerProps())) + expect(html).toContain('Copy') + expect(html).not.toContain('复制') + }) +})