diff --git a/apps/desktop/src/app/artifacts/index.tsx b/apps/desktop/src/app/artifacts/index.tsx index 049e0a437f4..5ae0cd42474 100644 --- a/apps/desktop/src/app/artifacts/index.tsx +++ b/apps/desktop/src/app/artifacts/index.tsx @@ -205,7 +205,6 @@ export function ArtifactsView({ setStatusbarItemGroup: _setStatusbarItemGroup, . // Rotating placeholder nudges from real data — search matches file paths and // session titles, not just labels; show it. - // TODO(i18n): literals until the UX settles. const searchHints = useMemo(() => { if (!artifacts?.length) { return undefined @@ -217,10 +216,10 @@ export function ArtifactsView({ setStatusbarItemGroup: _setStatusbarItemGroup, . const titles = [...new Set(artifacts.map(artifact => artifact.sessionTitle).filter(Boolean))].slice(0, 2) - const hints = [...extensions.map(ext => `Try “.${ext}”`), ...titles.map(title => `Try “${title}”`)] + const hints = [...extensions.map(ext => t.common.tryHint(`.${ext}`)), ...titles.map(title => t.common.tryHint(title))] return hints.length > 0 ? hints : undefined - }, [artifacts]) + }, [artifacts, t]) const counts = useMemo(() => { const all = artifacts || [] diff --git a/apps/desktop/src/app/cron/index.tsx b/apps/desktop/src/app/cron/index.tsx index 7989125afb7..a3d229ac5af 100644 --- a/apps/desktop/src/app/cron/index.tsx +++ b/apps/desktop/src/app/cron/index.tsx @@ -431,12 +431,11 @@ export function CronView({ onClose, onOpenSession, setStatusbarItemGroup: _setSt `Try “${title}”`)} + .map(title => t.common.tryHint(title))} searchLabel={c.search} searchPlaceholder={c.search} searchValue={query} diff --git a/apps/desktop/src/app/learning/archive-skill-confirm-dialog.tsx b/apps/desktop/src/app/learning/archive-skill-confirm-dialog.tsx index 298e890ee6a..5a4131c74f5 100644 --- a/apps/desktop/src/app/learning/archive-skill-confirm-dialog.tsx +++ b/apps/desktop/src/app/learning/archive-skill-confirm-dialog.tsx @@ -1,12 +1,12 @@ import { ConfirmDialog } from '@/components/ui/confirm-dialog' import { deleteLearningNode } from '@/hermes' +import { type Translations, useI18n } from '@/i18n' import { notify } from '@/store/notifications' export const ARCHIVE_SKILL_DESCRIPTION = 'The skill is archived and can be restored with `hermes curator restore`.' -export function notifySkillArchived(): void { - // TODO(i18n): literals until the UX settles. - notify({ kind: 'success', message: 'Restorable via hermes curator restore.', title: 'Skill archived' }) +export function notifySkillArchived(t: Translations): void { + notify({ kind: 'success', message: t.skills.skillArchivedMessage, title: t.skills.skillArchivedTitle }) } export async function archiveLearningSkill(id: string): Promise { @@ -46,6 +46,8 @@ export function ArchiveSkillConfirmDialog({ skillId, skillName }: ArchiveSkillConfirmDialogProps) { + const { t } = useI18n() + return ( { - notifySkillArchived() + notifySkillArchived(t) onSuccess?.() }), rollback, diff --git a/apps/desktop/src/app/master-detail.tsx b/apps/desktop/src/app/master-detail.tsx index ef204561259..4064f1b98fe 100644 --- a/apps/desktop/src/app/master-detail.tsx +++ b/apps/desktop/src/app/master-detail.tsx @@ -6,6 +6,7 @@ import { Codicon } from '@/components/ui/codicon' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { RowButton } from '@/components/ui/row-button' import { Switch } from '@/components/ui/switch' +import { useI18n } from '@/i18n' import { cn } from '@/lib/utils' import { $paneHeightOverride, $paneState, setPaneHeightOverride } from '@/store/panes' @@ -142,6 +143,7 @@ export function DetailPane({ onClose?: () => void title: ReactNode }) { + const { t } = useI18n() const override = useStore($paneHeightOverride(id)) useEffect(() => { @@ -196,8 +198,7 @@ export function DetailPane({ {actions} {onClose && ( - // TODO(i18n): literal until the UX settles. - )} diff --git a/apps/desktop/src/app/messaging/index.tsx b/apps/desktop/src/app/messaging/index.tsx index 13d68c63bea..cbf482d952c 100644 --- a/apps/desktop/src/app/messaging/index.tsx +++ b/apps/desktop/src/app/messaging/index.tsx @@ -269,8 +269,7 @@ export function MessagingView({ setStatusbarItemGroup: _setStatusbarItemGroup, . {...props} onSearchChange={setQuery} searchHidden={(platforms?.length ?? 0) === 0} - // TODO(i18n): literal until the UX settles. - searchHints={platforms?.slice(0, 5).map(platform => `Try “${platform.name.toLowerCase()}”`)} + searchHints={platforms?.slice(0, 5).map(platform => t.common.tryHint(platform.name.toLowerCase()))} searchPlaceholder={m.search} searchValue={query} > diff --git a/apps/desktop/src/app/skills/index.tsx b/apps/desktop/src/app/skills/index.tsx index 845a15f7c14..41620e2c124 100644 --- a/apps/desktop/src/app/skills/index.tsx +++ b/apps/desktop/src/app/skills/index.tsx @@ -102,13 +102,6 @@ const usageOf = (skill: SkillInfo): number => (typeof skill.usage === 'number' ? const categoryFor = (skill: SkillInfo): string => asText(skill.category) || 'general' -// TODO(i18n): literals until the UX settles. -const PROVENANCE_LABEL: Record, string> = { - agent: 'Learned', - bundled: 'Built-in', - hub: 'Hub' -} - // Row subtitle: category, with non-default origins badged. function skillSubtitle(skill: SkillInfo): React.ReactNode { const category = prettyName(categoryFor(skill)) @@ -295,7 +288,6 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p // Rotating placeholder nudges from the user's own data — teach that search // understands categories and tool names, not just titles. - // TODO(i18n): literals until the UX settles. const searchHints = useMemo(() => { if (mode === 'skills' && skills?.length) { const counts = new Map() @@ -308,18 +300,18 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p return [...counts.entries()] .sort(([, a], [, b]) => b - a) .slice(0, 5) - .map(([category]) => `Try “${category.toLowerCase()}”`) + .map(([category]) => t.common.tryHint(category.toLowerCase())) } if (mode === 'toolsets' && toolsets?.length) { return toolsets .filter(ts => isDesktopToolsetVisible(ts.name) && toolNames(ts).length > 0) .slice(0, 5) - .map(ts => `Try “${toolNames(ts)[0]}”`) + .map(ts => t.common.tryHint(toolNames(ts)[0])) } return undefined - }, [mode, skills, toolsets]) + }, [mode, skills, toolsets, t]) // Keep a valid selection: fall back to the first visible row when the // current selection is filtered out (or nothing is selected yet). @@ -422,35 +414,32 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p ) // One switch line covering enable-all/disable-all. - // TODO(i18n): literals until the UX settles. const bulkSwitch = (allEnabled: boolean): ListStripMenuToggle => ({ checked: allEnabled, disabled: bulkBusy, - label: 'All', + label: t.skills.all, onToggle: checked => void bulkToggle(checked) }) const allSkillsEnabled = bulkSkills.length > 0 && bulkSkills.every(s => s.enabled) const allToolsetsEnabled = bulkToolsets.length > 0 && bulkToolsets.every(ts => ts.enabled) - // TODO(i18n): literals until the UX settles. const sortButton = (desc: boolean, flip: () => void) => ( - {desc ? '↓ Most used' : '↑ Least used'} + {desc ? t.skills.sortMostUsedDesc : t.skills.sortLeastUsedAsc} ) // Full-bleed empty state, matching the MCP tab (spans both columns, not a // cramped note in the left rail). Query-aware, and says "tools" not the // internal "toolsets". - // TODO(i18n): literals until the UX settles. const capabilityEmpty = (noun: string) => { const q = query.trim() return (
) @@ -503,8 +492,7 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p try { await editLearningNode(skillEditor.name, skillDraft) - // TODO(i18n): literal until the UX settles. - notify({ kind: 'success', title: 'Skill updated', message: t.skills.appliesToNewSessions(skillEditor.name) }) + notify({ kind: 'success', title: t.skills.skillUpdated, message: t.skills.appliesToNewSessions(skillEditor.name) }) setSkillEditor(null) void refreshCapabilities() } catch (err) { @@ -518,8 +506,7 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p void saveSkillEdit()} size="xs"> - {/* TODO(i18n): literal until the UX settles. */} - {skillSaving ? t.common.saving : 'Save'} + {skillSaving ? t.common.saving : t.common.save} } id="skill-editor" @@ -590,7 +577,7 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p left={sortButton(skillsSortDesc, () => $skillsSortDesc.set(!$skillsSortDesc.get()))} right={ void disableUnused() }]} + items={[{ disabled: bulkBusy, label: t.skills.disableUnused, onSelect: () => void disableUnused() }]} label={t.skills.tabSkills} toggle={bulkSwitch(allSkillsEnabled)} /> @@ -613,8 +600,7 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p /> ))} - {/* TODO(i18n): literal until the UX settles. */} - + {activeSkill && ( setArchiveTarget(activeSkill.name)} @@ -665,8 +651,7 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p ) })} - {/* TODO(i18n): literal until the UX settles. */} - + {activeToolset && ( )} @@ -737,7 +722,7 @@ function SkillDetail({ onArchive, onEdit, skill }: { onArchive: () => void; onEd {prettyName(categoryFor(skill))} {skill.provenance && skill.provenance !== 'bundled' && ( - {PROVENANCE_LABEL[skill.provenance]} + {t.skills.provenance[skill.provenance]} )} @@ -746,12 +731,11 @@ function SkillDetail({ onArchive, onEdit, skill }: { onArchive: () => void; onEd /> {editable && (
- {/* TODO(i18n): literals until the UX settles. */}
)} diff --git a/apps/desktop/src/app/skills/mcp-tab.tsx b/apps/desktop/src/app/skills/mcp-tab.tsx index dca667ff2dc..6098a7a456e 100644 --- a/apps/desktop/src/app/skills/mcp-tab.tsx +++ b/apps/desktop/src/app/skills/mcp-tab.tsx @@ -36,7 +36,7 @@ import { saveMcpServers, testMcpServer } from '@/hermes' -import { useI18n } from '@/i18n' +import { type Translations, useI18n } from '@/i18n' import { countEnabledTools, isToolEnabled, toggleToolInServer } from '@/lib/mcp-tool-filter' import { cn } from '@/lib/utils' import { notify, notifyError } from '@/store/notifications' @@ -168,8 +168,11 @@ const STATUS_DOT: Record = { // the capabilities the server actually has. When a `server` config is passed, // the tool count reflects the per-tool include/exclude filter (what's actually // registered), not the raw discovered count. -// TODO(i18n): literals until the UX settles. -function capabilitySummary(probe: McpTestResult, server?: Record): string { +function capabilitySummary( + m: Translations['settings']['mcp'], + probe: McpTestResult, + server?: Record +): string { const toolCount = server ? countEnabledTools( server, @@ -177,32 +180,30 @@ function capabilitySummary(probe: McpTestResult, server?: Record): string { +function statusLine( + m: Translations['settings']['mcp'], + status: ServerStatus, + probe: Probe | undefined, + server?: Record +): string { switch (status) { case 'ok': - return capabilitySummary(probe as McpTestResult, server) + return capabilitySummary(m, probe as McpTestResult, server) case 'probing': - return 'Connecting…' + return m.statusConnecting case 'needs-auth': - return 'Needs authentication' + return m.statusNeedsAuth case 'error': - return 'Error' + return m.statusError case 'off': - return 'Off' + return m.statusOff default: return '' @@ -579,8 +580,11 @@ export function McpTab({ gateway }: { gateway: HermesGateway | null }) { resetDraft(nextServers) } - // TODO(i18n): literal until the UX settles. - notify({ kind: 'success', title: 'Authenticated', message: `${serverName}: ${result.tools.length} tools` }) + notify({ + kind: 'success', + title: m.authenticatedTitle, + message: m.authenticatedMessage(serverName, result.tools.length) + }) void silentReload() } else if (result.error) { notifyError(new Error(result.error), serverName) @@ -978,7 +982,7 @@ export function McpTab({ gateway }: { gateway: HermesGateway | null }) { onSelect={() => focusServer(serverName)} onToggle={checked => void toggleServer(serverName, checked)} status={status} - statusText={statusLine(status, probes[serverName], server)} + statusText={statusLine(m, status, probes[serverName], server)} /> ) })} @@ -1014,8 +1018,7 @@ export function McpTab({ gateway }: { gateway: HermesGateway | null }) { remountKey={docVersion} trailing={ } /> @@ -1037,13 +1040,12 @@ export function McpTab({ gateway }: { gateway: HermesGateway | null }) { defaultHeight={176} id="mcp-logs" title={ - // TODO(i18n): literal until the UX settles. - {selected && savedEntry ? selected : 'All servers'} + {selected && savedEntry ? selected : m.allServers} } > - +
@@ -1100,7 +1102,7 @@ function ServerConfig({ !hasHeaderAuth && (entry.auth === 'oauth' ? status === 'needs-auth' || status === 'error' : !entry.auth && status === 'needs-auth') - const summary = probe && probe !== 'probing' && probe.ok ? capabilitySummary(probe, entry) : null + const summary = probe && probe !== 'probing' && probe.ok ? capabilitySummary(m, probe, entry) : null return ( // p-2 matches the list view's container so flipping list ⇄ config keeps @@ -1112,12 +1114,11 @@ function ServerConfig({ mt-2.5, h-4 switch → mt-3.5) no matter how tall the text column gets. */}
)} - {!saved && ( - // TODO(i18n): literal until the UX settles. -

Unsaved — save mcp.json to connect.

- )} + {!saved &&

{m.unsavedConnect}

} {status === 'probing' && } @@ -1181,8 +1178,7 @@ function ServerConfig({
{/* Chip = a discovered tool; click to include/exclude it (struck through when excluded, so it won't register). The probe always - lists every tool regardless of the filter. - TODO(i18n): titles are literal until the UX settles. */} + lists every tool regardless of the filter. */} {probe.tools.map(tool => { const on = isToolEnabled(entry, tool.name) @@ -1197,7 +1193,7 @@ function ServerConfig({ disabled={!saved} key={tool.name} onClick={() => onToggleTool(tool.name)} - title={on ? `Disable ${tool.name}` : `Enable ${tool.name}`} + title={on ? m.disableTool(tool.name) : m.enableTool(tool.name)} type="button" > {tool.name} @@ -1483,7 +1479,15 @@ function filterStdioSections(lines: string[], server: string): string[] { // editor. Scope follows the cursor-selected server (all servers otherwise); // source controls live in the pane header. Body is the app's tool-output // surface: CodeCardBody typography + the floating hover-reveal copy button. -function McpLogs({ server, source }: { server: null | string; source: 'stdio' | 'agent' }) { +function McpLogs({ + emptyLabel, + server, + source +}: { + emptyLabel: string + server: null | string + source: 'stdio' | 'agent' +}) { const [lines, setLines] = useState(null) // A profile switch reroutes getLogs to the new backend; keying the effect on // the active profile tears down the old poll (its `cancelled` flag blocks a @@ -1518,8 +1522,7 @@ function McpLogs({ server, source }: { server: null | string; source: 'stdio' | } }, [server, source, activeProfile]) - // TODO(i18n): literal until the UX settles. - return + return } // --------------------------------------------------------------------------- diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index c769d9db7e6..dcfccaed707 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -25,6 +25,7 @@ export const en: Translations = { docs: 'Docs', done: 'Done', error: 'Error', + expand: 'Expand', failed: 'Failed', formatJson: 'Format JSON', free: 'Free', @@ -39,6 +40,7 @@ export const en: Translations = { set: 'Set', skip: 'Skip', update: 'Update', + tryHint: term => `Try “${term}”`, on: 'On', off: 'Off' }, @@ -629,7 +631,22 @@ export const en: Translations = { catalogInstallStarted: name => `Installing ${name}... applies to new sessions when done.`, catalogInstallFailed: name => `Failed to install ${name}`, catalogEnvPrompt: name => `${name} requires credentials`, - catalogEnvRequired: 'Fill in the required values before installing.' + catalogEnvRequired: 'Fill in the required values before installing.', + capabilitySummary: (tools, prompts, resources) => + `${[`${tools} tools`, ...(prompts ? [`${prompts} prompts`] : []), ...(resources ? [`${resources} resources`] : [])].join(', ')} enabled`, + statusConnecting: 'Connecting…', + statusNeedsAuth: 'Needs authentication', + statusError: 'Error', + statusOff: 'Off', + allServers: 'All servers', + authenticatedTitle: 'Authenticated', + authenticatedMessage: (server, count) => `${server}: ${count} tools`, + waitingForBrowser: 'Waiting for browser…', + authenticate: 'Authenticate', + unsavedConnect: 'Unsaved — save mcp.json to connect.', + enableTool: tool => `Enable ${tool}`, + disableTool: tool => `Disable ${tool}`, + noOutput: 'No output yet.' }, model: { loading: 'Loading model configuration...', @@ -785,11 +802,28 @@ export const en: Translations = { failedToUpdate: name => `Failed to update ${name}`, sortMostUsed: 'Most used', sortAlpha: 'A–Z', + sortMostUsedDesc: '↓ Most used', + sortLeastUsedAsc: '↑ Least used', enableAll: 'Enable all', disableAll: 'Disable all', + disableUnused: 'Disable unused', bulkUpdated: count => `Updated ${count} ${count === 1 ? 'item' : 'items'} for new sessions.`, bulkNoChange: 'Nothing to change.', usageCount: count => `used ${count}×`, + provenance: { + agent: 'Learned', + bundled: 'Built-in', + hub: 'Hub' + }, + emptyNoneFound: noun => `No ${noun} found`, + emptyNothingMatches: query => `Nothing matches “${query}”.`, + emptyNoneAvailable: noun => `No ${noun} available yet.`, + changesApplyNewSessions: 'Changes apply to new sessions.', + skillUpdated: 'Skill updated', + edit: 'Edit', + archive: 'Archive', + skillArchivedTitle: 'Skill archived', + skillArchivedMessage: 'Restorable via hermes curator restore.', hub: { searchPlaceholder: 'Search the skill hub', search: 'Search', diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index 465a623461c..9f4e3075699 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -25,6 +25,7 @@ export const ja = defineLocale({ docs: 'ドキュメント', done: '完了', error: 'エラー', + expand: '展開', failed: '失敗', formatJson: 'JSON を整形', free: '無料', @@ -39,6 +40,7 @@ export const ja = defineLocale({ set: '設定', skip: 'スキップ', update: '更新', + tryHint: term => `「${term}」を試す`, on: 'オン', off: 'オフ' }, @@ -725,7 +727,22 @@ export const ja = defineLocale({ name: '名前', serverJson: 'サーバー JSON', remove: '削除', - saveServer: 'サーバーを保存' + saveServer: 'サーバーを保存', + capabilitySummary: (tools, prompts, resources) => + `${[`ツール ${tools} 個`, ...(prompts ? [`プロンプト ${prompts} 個`] : []), ...(resources ? [`リソース ${resources} 個`] : [])].join('、')} を有効化`, + statusConnecting: '接続中…', + statusNeedsAuth: '認証が必要です', + statusError: 'エラー', + statusOff: 'オフ', + allServers: 'すべてのサーバー', + authenticatedTitle: '認証済み', + authenticatedMessage: (server, count) => `${server}: ツール ${count} 個`, + waitingForBrowser: 'ブラウザを待機中…', + authenticate: '認証', + unsavedConnect: '未保存 — 接続するには mcp.json を保存してください。', + enableTool: tool => `${tool} を有効化`, + disableTool: tool => `${tool} を無効化`, + noOutput: 'まだ出力がありません。' }, model: { loading: 'モデル設定を読み込み中...', @@ -864,11 +881,28 @@ export const ja = defineLocale({ failedToUpdate: name => `${name} の更新に失敗しました`, sortMostUsed: '使用頻度順', sortAlpha: 'A–Z', + sortMostUsedDesc: '↓ 使用頻度順', + sortLeastUsedAsc: '↑ 使用頻度が低い順', enableAll: 'すべて有効化', disableAll: 'すべて無効化', + disableUnused: '未使用を無効化', bulkUpdated: count => `${count} 件を新しいセッション向けに更新しました。`, bulkNoChange: '変更するものはありません。', - usageCount: count => `${count} 回使用` + usageCount: count => `${count} 回使用`, + provenance: { + agent: '学習済み', + bundled: '組み込み', + hub: 'ハブ' + }, + emptyNoneFound: noun => `${noun} が見つかりません`, + emptyNothingMatches: query => `「${query}」に一致するものはありません。`, + emptyNoneAvailable: noun => `利用可能な ${noun} はまだありません。`, + changesApplyNewSessions: '変更は新しいセッションに適用されます。', + skillUpdated: 'スキルを更新しました', + edit: '編集', + archive: 'アーカイブ', + skillArchivedTitle: 'スキルをアーカイブしました', + skillArchivedMessage: 'hermes curator restore で復元できます。' }, starmap: { diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts index 6248435a864..2d14c02d848 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -70,6 +70,7 @@ export interface Translations { docs: string done: string error: string + expand: string failed: string formatJson: string free: string @@ -84,6 +85,7 @@ export interface Translations { set: string skip: string update: string + tryHint: (term: string) => string on: string off: string } @@ -541,6 +543,20 @@ export interface Translations { catalogInstallFailed: (name: string) => string catalogEnvPrompt: (name: string) => string catalogEnvRequired: string + capabilitySummary: (tools: number, prompts: number, resources: number) => string + statusConnecting: string + statusNeedsAuth: string + statusError: string + statusOff: string + allServers: string + authenticatedTitle: string + authenticatedMessage: (server: string, count: number) => string + waitingForBrowser: string + authenticate: string + unsavedConnect: string + enableTool: (tool: string) => string + disableTool: (tool: string) => string + noOutput: string } model: { loading: string @@ -682,11 +698,24 @@ export interface Translations { failedToUpdate: (name: string) => string sortMostUsed: string sortAlpha: string + sortMostUsedDesc: string + sortLeastUsedAsc: string enableAll: string disableAll: string + disableUnused: string bulkUpdated: (count: number) => string bulkNoChange: string usageCount: (count: number | string) => string + provenance: Record<'agent' | 'bundled' | 'hub', string> + emptyNoneFound: (noun: string) => string + emptyNothingMatches: (query: string) => string + emptyNoneAvailable: (noun: string) => string + changesApplyNewSessions: string + skillUpdated: string + edit: string + archive: string + skillArchivedTitle: string + skillArchivedMessage: string hub: { searchPlaceholder: string search: string diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index 90c0f9e978c..0b14885c4ef 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -25,6 +25,7 @@ export const zhHant = defineLocale({ docs: '文件', done: '完成', error: '錯誤', + expand: '展開', failed: '失敗', formatJson: '格式化 JSON', free: '免費', @@ -39,6 +40,7 @@ export const zhHant = defineLocale({ set: '設定', skip: '略過', update: '更新', + tryHint: term => `試試「${term}」`, on: '開啟', off: '關閉' }, @@ -704,7 +706,22 @@ export const zhHant = defineLocale({ name: '名稱', serverJson: '伺服器 JSON', remove: '移除', - saveServer: '儲存伺服器' + saveServer: '儲存伺服器', + capabilitySummary: (tools, prompts, resources) => + `已啟用 ${[`${tools} 個工具`, ...(prompts ? [`${prompts} 個提示`] : []), ...(resources ? [`${resources} 個資源`] : [])].join('、')}`, + statusConnecting: '連線中…', + statusNeedsAuth: '需要驗證', + statusError: '錯誤', + statusOff: '關閉', + allServers: '所有伺服器', + authenticatedTitle: '已驗證', + authenticatedMessage: (server, count) => `${server}:${count} 個工具`, + waitingForBrowser: '等待瀏覽器…', + authenticate: '驗證', + unsavedConnect: '未儲存 — 儲存 mcp.json 以連線。', + enableTool: tool => `啟用 ${tool}`, + disableTool: tool => `停用 ${tool}`, + noOutput: '尚無輸出。' }, model: { loading: '正在載入模型設定...', @@ -836,11 +853,28 @@ export const zhHant = defineLocale({ failedToUpdate: name => `更新 ${name} 失敗`, sortMostUsed: '最常用', sortAlpha: 'A–Z', + sortMostUsedDesc: '↓ 最常用', + sortLeastUsedAsc: '↑ 最少用', enableAll: '全部啟用', disableAll: '全部停用', + disableUnused: '停用未使用', bulkUpdated: count => `已為新工作階段更新 ${count} 項。`, bulkNoChange: '沒有需要變更的內容。', - usageCount: count => `已使用 ${count} 次` + usageCount: count => `已使用 ${count} 次`, + provenance: { + agent: '已學習', + bundled: '內建', + hub: '技能中心' + }, + emptyNoneFound: noun => `找不到${noun}`, + emptyNothingMatches: query => `沒有符合「${query}」的內容。`, + emptyNoneAvailable: noun => `尚無可用的${noun}。`, + changesApplyNewSessions: '變更將套用至新工作階段。', + skillUpdated: '技能已更新', + edit: '編輯', + archive: '封存', + skillArchivedTitle: '技能已封存', + skillArchivedMessage: '可透過 hermes curator restore 還原。' }, starmap: { diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index 7e738fe05d2..6f6cebef790 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -25,6 +25,7 @@ export const zh: Translations = { docs: '文档', done: '完成', error: '错误', + expand: '展开', failed: '失败', formatJson: '格式化 JSON', free: '免费', @@ -39,6 +40,7 @@ export const zh: Translations = { set: '设置', skip: '跳过', update: '更新', + tryHint: term => `试试“${term}”`, on: '开', off: '关' }, @@ -818,7 +820,22 @@ export const zh: Translations = { catalogInstallStarted: name => `正在安装 ${name}… 完成后对新会话生效。`, catalogInstallFailed: name => `安装 ${name} 失败`, catalogEnvPrompt: name => `${name} 需要凭据`, - catalogEnvRequired: '安装前请填写必需的值。' + catalogEnvRequired: '安装前请填写必需的值。', + capabilitySummary: (tools, prompts, resources) => + `已启用 ${[`${tools} 个工具`, ...(prompts ? [`${prompts} 个提示`] : []), ...(resources ? [`${resources} 个资源`] : [])].join('、')}`, + statusConnecting: '连接中…', + statusNeedsAuth: '需要认证', + statusError: '错误', + statusOff: '关闭', + allServers: '所有服务器', + authenticatedTitle: '已认证', + authenticatedMessage: (server, count) => `${server}:${count} 个工具`, + waitingForBrowser: '等待浏览器…', + authenticate: '认证', + unsavedConnect: '未保存 — 保存 mcp.json 以连接。', + enableTool: tool => `启用 ${tool}`, + disableTool: tool => `禁用 ${tool}`, + noOutput: '暂无输出。' }, model: { loading: '正在加载模型配置...', @@ -969,11 +986,28 @@ export const zh: Translations = { failedToUpdate: name => `更新 ${name} 失败`, sortMostUsed: '最常用', sortAlpha: 'A–Z', + sortMostUsedDesc: '↓ 最常用', + sortLeastUsedAsc: '↑ 最少用', enableAll: '全部启用', disableAll: '全部停用', + disableUnused: '禁用未使用', bulkUpdated: count => `已为新会话更新 ${count} 项。`, bulkNoChange: '没有需要更改的内容。', usageCount: count => `已使用 ${count} 次`, + provenance: { + agent: '习得', + bundled: '内置', + hub: '技能中心' + }, + emptyNoneFound: noun => `未找到${noun}`, + emptyNothingMatches: query => `没有匹配“${query}”的内容。`, + emptyNoneAvailable: noun => `暂无可用的${noun}。`, + changesApplyNewSessions: '更改将应用于新会话。', + skillUpdated: '技能已更新', + edit: '编辑', + archive: '归档', + skillArchivedTitle: '技能已归档', + skillArchivedMessage: '可通过 hermes curator restore 恢复。', hub: { searchPlaceholder: '搜索技能中心', search: '搜索',