refactor(desktop): localize settled TODO(i18n) literals (#57924)

The Capabilities/MCP/Hub/Skills UX has settled, so lift every
`// TODO(i18n): literal until the UX settles` hardcoded English string into
the typed i18n catalog and drop the comments.

- New keys under `common` (expand, tryHint), `settings.mcp` (capability
  summary, status line, all-servers, auth flow, tool chip titles, log empty
  label), and `skills` (provenance, sort/bulk labels, empty states, editor
  actions). Full translations in en + zh; ja + zh-hant overrides added.
- Module-level pure fns that had no `t` in scope now take the mcp translations
  (`capabilitySummary`/`statusLine`) or an `emptyLabel` prop (`McpLogs`); the
  archive toast takes `t`.
- Shared `common.tryHint(term)` dedupes the "Try “…”" search hint across
  skills/messaging/cron/artifacts.

No behavior or styling change — string lookups only. Zero TODO(i18n) remain.
This commit is contained in:
brooklyn! 2026-07-03 15:43:50 -05:00 committed by GitHub
parent 20c83af664
commit 86518638a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 244 additions and 93 deletions

View file

@ -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 || []

View file

@ -431,12 +431,11 @@ export function CronView({ onClose, onOpenSession, setStatusbarItemGroup: _setSt
<PanelBody>
<PanelList
onSearchChange={setQuery}
// TODO(i18n): literal until the UX settles.
searchHints={jobs
.map(jobTitle)
.filter(Boolean)
.slice(0, 5)
.map(title => `Try “${title}`)}
.map(title => t.common.tryHint(title))}
searchLabel={c.search}
searchPlaceholder={c.search}
searchValue={query}

View file

@ -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<void> {
@ -46,6 +46,8 @@ export function ArchiveSkillConfirmDialog({
skillId,
skillName
}: ArchiveSkillConfirmDialogProps) {
const { t } = useI18n()
return (
<ConfirmDialog
confirmLabel="Archive"
@ -58,7 +60,7 @@ export function ArchiveSkillConfirmDialog({
fireOptimistic(
archiveLearningSkill(skillId).then(() => {
notifySkillArchived()
notifySkillArchived(t)
onSuccess?.()
}),
rollback,

View file

@ -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}
<Button
aria-expanded={!collapsed}
// TODO(i18n): literals until the UX settles.
aria-label={collapsed ? 'Expand' : 'Collapse'}
aria-label={collapsed ? t.common.expand : t.common.collapse}
className={ICON_BUTTON}
onClick={() => setPaneHeightOverride(id, collapsed ? undefined : 0)}
size="icon"
@ -206,8 +207,7 @@ export function DetailPane({
<Codicon name={collapsed ? 'chevron-up' : 'chevron-down'} size="0.8125rem" />
</Button>
{onClose && (
// TODO(i18n): literal until the UX settles.
<Button aria-label="Close" className={ICON_BUTTON} onClick={onClose} size="icon" variant="ghost">
<Button aria-label={t.common.close} className={ICON_BUTTON} onClick={onClose} size="icon" variant="ghost">
<Codicon name="close" size="0.8125rem" />
</Button>
)}

View file

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

View file

@ -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<NonNullable<SkillInfo['provenance']>, 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<string, number>()
@ -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) => (
<ListStripButton onClick={flip}>{desc ? '↓ Most used' : '↑ Least used'}</ListStripButton>
<ListStripButton onClick={flip}>{desc ? t.skills.sortMostUsedDesc : t.skills.sortLeastUsedAsc}</ListStripButton>
)
// 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 (
<div className="flex h-full min-h-0 flex-1">
<PanelEmpty
description={q ? `Nothing matches “${q}”.` : `No ${noun} available yet.`}
description={q ? t.skills.emptyNothingMatches(q) : t.skills.emptyNoneAvailable(noun)}
icon="search"
title={`No ${noun} found`}
title={t.skills.emptyNoneFound(noun)}
/>
</div>
)
@ -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
<DetailPane
actions={
<Button disabled={skillSaving} onClick={() => void saveSkillEdit()} size="xs">
{/* TODO(i18n): literal until the UX settles. */}
{skillSaving ? t.common.saving : 'Save'}
{skillSaving ? t.common.saving : t.common.save}
</Button>
}
id="skill-editor"
@ -590,7 +577,7 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p
left={sortButton(skillsSortDesc, () => $skillsSortDesc.set(!$skillsSortDesc.get()))}
right={
<ListStripMenu
items={[{ disabled: bulkBusy, label: 'Disable unused', onSelect: () => 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
/>
))}
</ListColumn>
{/* TODO(i18n): literal until the UX settles. */}
<DetailColumn footer="Changes apply to new sessions.">
<DetailColumn footer={t.skills.changesApplyNewSessions}>
{activeSkill && (
<SkillDetail
onArchive={() => setArchiveTarget(activeSkill.name)}
@ -665,8 +651,7 @@ export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...p
)
})}
</ListColumn>
{/* TODO(i18n): literal until the UX settles. */}
<DetailColumn footer="Changes apply to new sessions.">
<DetailColumn footer={t.skills.changesApplyNewSessions}>
{activeToolset && (
<ToolsetDetail onConfiguredChange={refreshToolsets} toolCalls={toolCalls ?? {}} toolset={activeToolset} />
)}
@ -737,7 +722,7 @@ function SkillDetail({ onArchive, onEdit, skill }: { onArchive: () => void; onEd
<PanelPill>{prettyName(categoryFor(skill))}</PanelPill>
{skill.provenance && skill.provenance !== 'bundled' && (
<PanelPill tone={skill.provenance === 'agent' ? 'good' : 'muted'}>
{PROVENANCE_LABEL[skill.provenance]}
{t.skills.provenance[skill.provenance]}
</PanelPill>
)}
</>
@ -746,12 +731,11 @@ function SkillDetail({ onArchive, onEdit, skill }: { onArchive: () => void; onEd
/>
{editable && (
<div className="flex items-center gap-2">
{/* TODO(i18n): literals until the UX settles. */}
<Button onClick={onEdit} size="xs" variant="text">
Edit
{t.skills.edit}
</Button>
<Button className="text-destructive hover:text-destructive" onClick={onArchive} size="xs" variant="text">
Archive
{t.skills.archive}
</Button>
</div>
)}

View file

@ -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<ServerStatus, string> = {
// 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, unknown>): string {
function capabilitySummary(
m: Translations['settings']['mcp'],
probe: McpTestResult,
server?: Record<string, unknown>
): string {
const toolCount = server
? countEnabledTools(
server,
@ -177,32 +180,30 @@ function capabilitySummary(probe: McpTestResult, server?: Record<string, unknown
)
: probe.tools.length
const parts = [
`${toolCount} tools`,
...(probe.prompts ? [`${probe.prompts} prompts`] : []),
...(probe.resources ? [`${probe.resources} resources`] : [])
]
return `${parts.join(', ')} enabled`
return m.capabilitySummary(toolCount, probe.prompts ?? 0, probe.resources ?? 0)
}
// TODO(i18n): literals until the UX settles.
function statusLine(status: ServerStatus, probe: Probe | undefined, server?: Record<string, unknown>): string {
function statusLine(
m: Translations['settings']['mcp'],
status: ServerStatus,
probe: Probe | undefined,
server?: Record<string, unknown>
): 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={
<Button disabled={saving || !dirty} onClick={() => void saveDoc()} size="xs">
{/* TODO(i18n): literal until the UX settles. */}
{saving ? t.common.saving : 'Save'}
{saving ? t.common.saving : t.common.save}
</Button>
}
/>
@ -1037,13 +1040,12 @@ export function McpTab({ gateway }: { gateway: HermesGateway | null }) {
defaultHeight={176}
id="mcp-logs"
title={
// TODO(i18n): literal until the UX settles.
<span className="text-[0.68rem] font-normal text-muted-foreground/60">
{selected && savedEntry ? selected : 'All servers'}
{selected && savedEntry ? selected : m.allServers}
</span>
}
>
<McpLogs server={selected && savedEntry ? selected : null} source={logSource} />
<McpLogs emptyLabel={m.noOutput} server={selected && savedEntry ? selected : null} source={logSource} />
</DetailPane>
</main>
</div>
@ -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. */}
<div className="flex items-start gap-2 pr-1.5">
<Button
// TODO(i18n): literal until the UX settles.
aria-label="All servers"
aria-label={m.allServers}
className={cn('mt-3', ICON_BUTTON)}
onClick={onBack}
size="icon"
title="All servers"
title={m.allServers}
variant="ghost"
>
<Codicon name="chevron-left" size="0.8125rem" />
@ -1161,15 +1162,11 @@ function ServerConfig({
{canAuth && saved && (
<div className="mt-3 flex justify-end">
<Button disabled={authing} onClick={onAuthenticate} size="xs">
{/* TODO(i18n): literals until the UX settles. */}
{authing ? 'Waiting for browser…' : 'Authenticate'}
{authing ? m.waitingForBrowser : m.authenticate}
</Button>
</div>
)}
{!saved && (
// TODO(i18n): literal until the UX settles.
<p className="mt-3 text-[0.68rem] text-muted-foreground/60">Unsaved save mcp.json to connect.</p>
)}
{!saved && <p className="mt-3 text-[0.68rem] text-muted-foreground/60">{m.unsavedConnect}</p>}
{status === 'probing' && <PageLoader className="min-h-24" label={t.skills.loading} />}
@ -1181,8 +1178,7 @@ function ServerConfig({
<div className="mt-3 flex flex-wrap gap-1">
{/* 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 | string[]>(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 <LogTail emptyLabel="No output yet." lines={lines} />
return <LogTail emptyLabel={emptyLabel} lines={lines} />
}
// ---------------------------------------------------------------------------

View file

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

View file

@ -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: 'AZ',
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: {

View file

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

View file

@ -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: 'AZ',
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: {

View file

@ -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: 'AZ',
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: '搜索',