fix(web): localize mention picker copy (#3255)

This commit is contained in:
初晨 2026-05-29 11:19:14 +08:00 committed by GitHub
parent 5319e14dc0
commit 9c6a69490b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 144 additions and 24 deletions

View file

@ -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 (

View file

@ -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',

View file

@ -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': '该文件夹已关联',

View file

@ -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': '該資料夾已關聯',

View file

@ -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;

View file

@ -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;