mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
fix(web): localize mention picker copy (#3255)
This commit is contained in:
parent
5319e14dc0
commit
9c6a69490b
6 changed files with 144 additions and 24 deletions
|
|
@ -2426,8 +2426,8 @@ function mcpTemplateMatchesQuery(tpl: McpTemplate, query: string): boolean {
|
|||
.includes(q);
|
||||
}
|
||||
|
||||
function pluginSourceLabel(plugin: InstalledPluginRecord): string {
|
||||
return plugin.sourceKind === 'bundled' ? 'Official' : 'My plugin';
|
||||
function pluginSourceLabel(plugin: InstalledPluginRecord, t: TranslateFn): string {
|
||||
return plugin.sourceKind === 'bundled' ? t('chat.mentionPluginOfficial') : t('chat.mentionPluginMine');
|
||||
}
|
||||
|
||||
function ToolsImportPanel({
|
||||
|
|
@ -2574,16 +2574,16 @@ function MentionPopover({
|
|||
onPickMcp: (server: McpServerConfig) => void;
|
||||
onPickConnector: (connector: ConnectorDetail) => void;
|
||||
}) {
|
||||
const { locale } = useI18n();
|
||||
const { locale, t } = useI18n();
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [tab, setTab] = useState<MentionTab>('all');
|
||||
const tabs: Array<{ id: MentionTab; label: string }> = [
|
||||
{ id: 'all', label: 'All' },
|
||||
{ id: 'plugins', label: 'Plugins' },
|
||||
{ id: 'skills', label: 'Skills' },
|
||||
{ id: 'mcp', label: 'MCP' },
|
||||
{ id: 'connectors', label: 'Connectors' },
|
||||
{ id: 'files', label: 'Design files' },
|
||||
{ id: 'all', label: t('chat.mentionTabAll') },
|
||||
{ id: 'plugins', label: t('chat.mentionTabPlugins') },
|
||||
{ id: 'skills', label: t('chat.mentionTabSkills') },
|
||||
{ id: 'mcp', label: t('chat.mentionTabMcp') },
|
||||
{ id: 'connectors', label: t('chat.mentionTabConnectors') },
|
||||
{ id: 'files', label: t('chat.mentionTabFiles') },
|
||||
];
|
||||
const showPlugins = tab === 'all' || tab === 'plugins';
|
||||
const showSkills = tab === 'all' || tab === 'skills';
|
||||
|
|
@ -2601,7 +2601,7 @@ function MentionPopover({
|
|||
}, [connectors, files, plugins, skills, mcpServers, tab]);
|
||||
return (
|
||||
<div className="mention-popover" data-testid="mention-popover">
|
||||
<div className="mention-tabs" role="tablist" aria-label="Mention surfaces">
|
||||
<div className="mention-tabs" role="tablist" aria-label={t('chat.mentionTabsAria')}>
|
||||
{tabs.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
|
|
@ -2620,15 +2620,15 @@ function MentionPopover({
|
|||
{!hasVisibleResults ? (
|
||||
<div className="mention-empty">
|
||||
{query ? (
|
||||
<>No results for “{query}”.</>
|
||||
<>{t('chat.mentionNoResults', { query })}</>
|
||||
) : (
|
||||
<>Search plugins, skills, MCP servers, connectors, and Design Files.</>
|
||||
<>{t('chat.mentionSearchPrompt')}</>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{showPlugins && plugins.length > 0 ? (
|
||||
<>
|
||||
<div className="mention-section-label">Plugins</div>
|
||||
<div className="mention-section-label">{t('chat.mentionSectionPlugins')}</div>
|
||||
{plugins.map((p) => (
|
||||
<button
|
||||
key={`plugin-${p.id}`}
|
||||
|
|
@ -2645,14 +2645,14 @@ function MentionPopover({
|
|||
{p.manifest?.description ?? p.id}
|
||||
</span>
|
||||
</span>
|
||||
<span className="mention-meta">{pluginSourceLabel(p)}</span>
|
||||
<span className="mention-meta">{pluginSourceLabel(p, t)}</span>
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
) : null}
|
||||
{showSkills && skills.length > 0 ? (
|
||||
<>
|
||||
<div className="mention-section-label">Skills</div>
|
||||
<div className="mention-section-label">{t('chat.mentionSectionSkills')}</div>
|
||||
{skills.map((skill) => {
|
||||
const active = skill.id === currentSkillId;
|
||||
return (
|
||||
|
|
@ -2671,7 +2671,7 @@ function MentionPopover({
|
|||
{localizeSkillDescription(locale, skill) || skill.id}
|
||||
</span>
|
||||
</span>
|
||||
<span className="mention-meta">{active ? 'Active' : skill.mode}</span>
|
||||
<span className="mention-meta">{active ? t('chat.mentionActiveSkill') : skill.mode}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
|
@ -2679,7 +2679,7 @@ function MentionPopover({
|
|||
) : null}
|
||||
{showMcp && mcpServers.length > 0 ? (
|
||||
<>
|
||||
<div className="mention-section-label">MCP</div>
|
||||
<div className="mention-section-label">{t('chat.mentionSectionMcp')}</div>
|
||||
{mcpServers.map((server) => (
|
||||
<button
|
||||
key={`mcp-${server.id}`}
|
||||
|
|
@ -2687,7 +2687,7 @@ function MentionPopover({
|
|||
type="button"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onPickMcp(server)}
|
||||
title={`Use ${server.label || server.id}`}
|
||||
title={t('chat.mentionUseMcpTitle', { name: server.label || server.id })}
|
||||
>
|
||||
<Icon name="link" size={12} />
|
||||
<span className="mention-item-body">
|
||||
|
|
@ -2703,7 +2703,7 @@ function MentionPopover({
|
|||
) : null}
|
||||
{showConnectors && connectors.length > 0 ? (
|
||||
<>
|
||||
<div className="mention-section-label">Connectors</div>
|
||||
<div className="mention-section-label">{t('chat.mentionSectionConnectors')}</div>
|
||||
{connectors.map((connector) => (
|
||||
<button
|
||||
key={`connector-${connector.id}`}
|
||||
|
|
@ -2711,7 +2711,7 @@ function MentionPopover({
|
|||
type="button"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onPickConnector(connector)}
|
||||
title={`Use ${connector.name}`}
|
||||
title={t('chat.mentionUseConnectorTitle', { name: connector.name })}
|
||||
>
|
||||
<Icon name="link" size={12} />
|
||||
<span className="mention-item-body">
|
||||
|
|
@ -2727,7 +2727,7 @@ function MentionPopover({
|
|||
) : null}
|
||||
{showFiles && files.length > 0 ? (
|
||||
<>
|
||||
<div className="mention-section-label">Design files</div>
|
||||
<div className="mention-section-label">{t('chat.mentionSectionFiles')}</div>
|
||||
{files.map((f) => {
|
||||
const key = f.path ?? f.name;
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1470,6 +1470,25 @@ export const en: Dict = {
|
|||
'chat.importFolder': 'Link code folder',
|
||||
'chat.importSkills': 'Skills and design systems',
|
||||
'chat.importProject': 'Reference another project',
|
||||
'chat.mentionTabsAria': 'Mention surfaces',
|
||||
'chat.mentionTabAll': 'All',
|
||||
'chat.mentionTabPlugins': 'Plugins',
|
||||
'chat.mentionTabSkills': 'Skills',
|
||||
'chat.mentionTabMcp': 'MCP',
|
||||
'chat.mentionTabConnectors': 'Connectors',
|
||||
'chat.mentionTabFiles': 'Design files',
|
||||
'chat.mentionNoResults': 'No results for “{query}”.',
|
||||
'chat.mentionSearchPrompt': 'Search plugins, skills, MCP servers, connectors, and Design Files.',
|
||||
'chat.mentionSectionPlugins': 'Plugins',
|
||||
'chat.mentionSectionSkills': 'Skills',
|
||||
'chat.mentionSectionMcp': 'MCP',
|
||||
'chat.mentionSectionConnectors': 'Connectors',
|
||||
'chat.mentionSectionFiles': 'Design files',
|
||||
'chat.mentionActiveSkill': 'Active',
|
||||
'chat.mentionUseMcpTitle': 'Use {name}',
|
||||
'chat.mentionUseConnectorTitle': 'Use {name}',
|
||||
'chat.mentionPluginOfficial': 'Official',
|
||||
'chat.mentionPluginMine': 'My plugin',
|
||||
'chat.linkedFolderRemoveAria': 'Remove linked folder {path}',
|
||||
'chat.linkedFolderNotFound': 'Folder does not exist',
|
||||
'chat.linkedFolderAlready': 'This folder is already linked',
|
||||
|
|
|
|||
|
|
@ -1461,6 +1461,25 @@ export const zhCN: Dict = {
|
|||
'chat.importFolder': '关联代码目录',
|
||||
'chat.importSkills': '技能与设计体系',
|
||||
'chat.importProject': '引用其它项目',
|
||||
'chat.mentionTabsAria': '提及来源',
|
||||
'chat.mentionTabAll': '全部',
|
||||
'chat.mentionTabPlugins': '插件',
|
||||
'chat.mentionTabSkills': '技能',
|
||||
'chat.mentionTabMcp': 'MCP',
|
||||
'chat.mentionTabConnectors': '连接器',
|
||||
'chat.mentionTabFiles': '设计文件',
|
||||
'chat.mentionNoResults': '没有找到“{query}”的结果。',
|
||||
'chat.mentionSearchPrompt': '搜索插件、技能、MCP 服务器、连接器和设计文件。',
|
||||
'chat.mentionSectionPlugins': '插件',
|
||||
'chat.mentionSectionSkills': '技能',
|
||||
'chat.mentionSectionMcp': 'MCP',
|
||||
'chat.mentionSectionConnectors': '连接器',
|
||||
'chat.mentionSectionFiles': '设计文件',
|
||||
'chat.mentionActiveSkill': '已启用',
|
||||
'chat.mentionUseMcpTitle': '使用 {name}',
|
||||
'chat.mentionUseConnectorTitle': '使用 {name}',
|
||||
'chat.mentionPluginOfficial': '官方',
|
||||
'chat.mentionPluginMine': '我的插件',
|
||||
'chat.linkedFolderRemoveAria': '移除关联文件夹 {path}',
|
||||
'chat.linkedFolderNotFound': '文件夹不存在',
|
||||
'chat.linkedFolderAlready': '该文件夹已关联',
|
||||
|
|
|
|||
|
|
@ -1061,6 +1061,25 @@ export const zhTW: Dict = {
|
|||
'chat.importFolder': '關聯程式碼目錄',
|
||||
'chat.importSkills': '技能與設計系統',
|
||||
'chat.importProject': '引用其它專案',
|
||||
'chat.mentionTabsAria': '提及來源',
|
||||
'chat.mentionTabAll': '全部',
|
||||
'chat.mentionTabPlugins': '外掛',
|
||||
'chat.mentionTabSkills': '技能',
|
||||
'chat.mentionTabMcp': 'MCP',
|
||||
'chat.mentionTabConnectors': '連接器',
|
||||
'chat.mentionTabFiles': '設計檔案',
|
||||
'chat.mentionNoResults': '找不到「{query}」的結果。',
|
||||
'chat.mentionSearchPrompt': '搜尋外掛、技能、MCP 伺服器、連接器和設計檔案。',
|
||||
'chat.mentionSectionPlugins': '外掛',
|
||||
'chat.mentionSectionSkills': '技能',
|
||||
'chat.mentionSectionMcp': 'MCP',
|
||||
'chat.mentionSectionConnectors': '連接器',
|
||||
'chat.mentionSectionFiles': '設計檔案',
|
||||
'chat.mentionActiveSkill': '已啟用',
|
||||
'chat.mentionUseMcpTitle': '使用 {name}',
|
||||
'chat.mentionUseConnectorTitle': '使用 {name}',
|
||||
'chat.mentionPluginOfficial': '官方',
|
||||
'chat.mentionPluginMine': '我的外掛',
|
||||
'chat.linkedFolderRemoveAria': '移除關聯資料夾 {path}',
|
||||
'chat.linkedFolderNotFound': '資料夾不存在',
|
||||
'chat.linkedFolderAlready': '該資料夾已關聯',
|
||||
|
|
|
|||
|
|
@ -1781,6 +1781,25 @@ export interface Dict {
|
|||
'chat.importFolder': string;
|
||||
'chat.importSkills': string;
|
||||
'chat.importProject': string;
|
||||
'chat.mentionTabsAria': string;
|
||||
'chat.mentionTabAll': string;
|
||||
'chat.mentionTabPlugins': string;
|
||||
'chat.mentionTabSkills': string;
|
||||
'chat.mentionTabMcp': string;
|
||||
'chat.mentionTabConnectors': string;
|
||||
'chat.mentionTabFiles': string;
|
||||
'chat.mentionNoResults': string;
|
||||
'chat.mentionSearchPrompt': string;
|
||||
'chat.mentionSectionPlugins': string;
|
||||
'chat.mentionSectionSkills': string;
|
||||
'chat.mentionSectionMcp': string;
|
||||
'chat.mentionSectionConnectors': string;
|
||||
'chat.mentionSectionFiles': string;
|
||||
'chat.mentionActiveSkill': string;
|
||||
'chat.mentionUseMcpTitle': string;
|
||||
'chat.mentionUseConnectorTitle': string;
|
||||
'chat.mentionPluginOfficial': string;
|
||||
'chat.mentionPluginMine': string;
|
||||
'chat.linkedFolderRemoveAria': string;
|
||||
'chat.linkedFolderNotFound': string;
|
||||
'chat.linkedFolderAlready': string;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import type { ComponentProps } from 'react';
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ChatComposer } from '../../src/components/ChatComposer';
|
||||
import { I18nProvider } from '../../src/i18n';
|
||||
import type { Locale } from '../../src/i18n/types';
|
||||
|
||||
const COMMUNITY_PLUGIN = {
|
||||
id: 'community-deck',
|
||||
|
|
@ -115,8 +117,11 @@ let plugins = [COMMUNITY_PLUGIN, USER_PLUGIN];
|
|||
let skills = [SKILL];
|
||||
let servers = [MCP_SERVER];
|
||||
|
||||
function renderComposer(overrides: Partial<ComponentProps<typeof ChatComposer>> = {}) {
|
||||
return render(
|
||||
function renderComposer(
|
||||
overrides: Partial<ComponentProps<typeof ChatComposer>> = {},
|
||||
options: { locale?: Locale } = {},
|
||||
) {
|
||||
const tree = (
|
||||
<ChatComposer
|
||||
projectId="project-1"
|
||||
projectFiles={[]}
|
||||
|
|
@ -127,7 +132,19 @@ function renderComposer(overrides: Partial<ComponentProps<typeof ChatComposer>>
|
|||
onOpenMcpSettings={vi.fn()}
|
||||
skills={skills}
|
||||
{...overrides}
|
||||
/>,
|
||||
/>
|
||||
);
|
||||
|
||||
if (options.locale) {
|
||||
return render(
|
||||
<I18nProvider initial={options.locale}>
|
||||
{tree}
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
return render(
|
||||
tree,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -196,6 +213,33 @@ describe('ChatComposer context pickers', () => {
|
|||
expect(screen.getByText('Search plugins, skills, MCP servers, connectors, and Design Files.')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('localizes @ panel tabs and empty states in Chinese mode', async () => {
|
||||
plugins = [];
|
||||
skills = [];
|
||||
servers = [];
|
||||
renderComposer({}, { locale: 'zh-CN' });
|
||||
const input = screen.getByTestId('chat-composer-input') as HTMLTextAreaElement;
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: { value: '@', selectionStart: 1 },
|
||||
});
|
||||
|
||||
expect(screen.getByRole('tab', { name: '全部' })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: '插件' })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: '技能' })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: 'MCP' })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: '连接器' })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: '设计文件' })).toBeTruthy();
|
||||
expect(screen.getByText('搜索插件、技能、MCP 服务器、连接器和设计文件。')).toBeTruthy();
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: { value: '@missing', selectionStart: 8 },
|
||||
});
|
||||
|
||||
expect(screen.getByText('没有找到“missing”的结果。')).toBeTruthy();
|
||||
expect(screen.queryByText('No results for “missing”.')).toBeNull();
|
||||
});
|
||||
|
||||
it('selects an MCP server from @ search and keeps the inline token visible', async () => {
|
||||
renderComposer();
|
||||
const input = screen.getByTestId('chat-composer-input') as HTMLTextAreaElement;
|
||||
|
|
|
|||
Loading…
Reference in a new issue