diff --git a/ui/src/lib/skills-grouping.ts b/ui/src/lib/skills-grouping.ts index fdd3026be4dd..7c2b0fc648f0 100644 --- a/ui/src/lib/skills-grouping.ts +++ b/ui/src/lib/skills-grouping.ts @@ -1,5 +1,6 @@ // Shared pure skill grouping helper. import type { SkillStatusEntry } from "../api/types.ts"; +import { t } from "../i18n/index.ts"; export type SkillGroup = { id: string; @@ -7,20 +8,20 @@ export type SkillGroup = { skills: SkillStatusEntry[]; }; -const SKILL_SOURCE_GROUPS: Array<{ id: string; label: string; sources: string[] }> = [ - { id: "workspace", label: "Workspace Skills", sources: ["openclaw-workspace"] }, - { id: "built-in", label: "Built-in Skills", sources: ["openclaw-bundled"] }, - { id: "installed", label: "Installed Skills", sources: ["openclaw-managed"] }, - { id: "extra", label: "Extra Skills", sources: ["openclaw-extra"] }, +const SKILL_SOURCE_GROUPS: Array<{ id: string; labelKey: string; sources: string[] }> = [ + { id: "workspace", labelKey: "skillGroups.workspace", sources: ["openclaw-workspace"] }, + { id: "built-in", labelKey: "skillGroups.builtIn", sources: ["openclaw-bundled"] }, + { id: "installed", labelKey: "skillGroups.installed", sources: ["openclaw-managed"] }, + { id: "extra", labelKey: "skillGroups.extra", sources: ["openclaw-extra"] }, ]; export function groupSkills(skills: SkillStatusEntry[]): SkillGroup[] { const groups = new Map(); for (const def of SKILL_SOURCE_GROUPS) { - groups.set(def.id, { id: def.id, label: def.label, skills: [] }); + groups.set(def.id, { id: def.id, label: t(def.labelKey), skills: [] }); } const builtInGroup = SKILL_SOURCE_GROUPS.find((group) => group.id === "built-in"); - const other: SkillGroup = { id: "other", label: "Other Skills", skills: [] }; + const other: SkillGroup = { id: "other", label: t("skillGroups.other"), skills: [] }; for (const skill of skills) { const match = skill.bundled ? builtInGroup diff --git a/ui/src/lib/skills-shared.ts b/ui/src/lib/skills-shared.ts index 65382d0f9209..ab54d4c8f5da 100644 --- a/ui/src/lib/skills-shared.ts +++ b/ui/src/lib/skills-shared.ts @@ -1,6 +1,7 @@ // Shared skill status rendering and classification helpers. import { html, nothing } from "lit"; import type { SkillStatusEntry } from "../api/types.ts"; +import { t } from "../i18n/index.ts"; export function computeSkillMissing(skill: SkillStatusEntry): string[] { return [ @@ -14,13 +15,13 @@ export function computeSkillMissing(skill: SkillStatusEntry): string[] { export function computeSkillReasons(skill: SkillStatusEntry): string[] { const reasons: string[] = []; if (skill.disabled) { - reasons.push("disabled"); + reasons.push(t("skillStatus.disabled")); } if (skill.blockedByAllowlist) { - reasons.push("blocked by allowlist"); + reasons.push(t("skillStatus.blockedAllowlist")); } if (skill.blockedByAgentFilter) { - reasons.push("blocked by agent filter"); + reasons.push(t("skillStatus.blockedAgentFilter")); } return reasons; } @@ -39,11 +40,13 @@ export function renderSkillStatusChips(params: { return html`
${skill.source} - ${showBundledBadge ? html` bundled ` : nothing} + ${showBundledBadge ? html` ${t("skillStatus.bundled")} ` : nothing} - ${available ? "eligible" : "blocked"} + ${available ? t("skillStatus.eligible") : t("skillStatus.blocked")} - ${skill.disabled ? html` disabled ` : nothing} + ${skill.disabled + ? html` ${t("skillStatus.disabled")} ` + : nothing}
`; } diff --git a/ui/src/pages/plugins/presentation.ts b/ui/src/pages/plugins/presentation.ts index f3f7707557f4..7a050341fb26 100644 --- a/ui/src/pages/plugins/presentation.ts +++ b/ui/src/pages/plugins/presentation.ts @@ -275,15 +275,14 @@ export const CONNECTOR_GROUP_ORDER: readonly ConnectorGroup[] = ["work", "dev", export type ConnectorSuggestion = { id: string; name: string; - description: string; + descriptionKey: string; group: ConnectorGroup; action: { kind: "mcp"; mcp: ConnectorMcpTemplate } | { kind: "clawhub"; query: string }; }; /** * Curated connector shelf: one-click MCP servers for official hosted endpoints - * plus ClawHub searches proven to return live packages. Descriptions are - * catalog data (like manifest descriptions), not localized UI chrome. + * plus ClawHub searches proven to return live packages. */ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ // --- Work & productivity --- @@ -291,7 +290,7 @@ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ id: "notion", group: "work", name: "Notion", - description: "Search, create, and update pages and databases in your Notion workspace.", + descriptionKey: "pluginsPage.connectorDescriptions.notion", action: { kind: "mcp", mcp: { @@ -306,7 +305,7 @@ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ id: "linear", group: "work", name: "Linear", - description: "Triage issues, update cycles, and file bugs straight from chat.", + descriptionKey: "pluginsPage.connectorDescriptions.linear", action: { kind: "mcp", mcp: { @@ -322,7 +321,7 @@ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ id: "todoist", group: "work", name: "Todoist", - description: "Read, add, and complete tasks and projects in Todoist.", + descriptionKey: "pluginsPage.connectorDescriptions.todoist", action: { kind: "mcp", mcp: { @@ -338,7 +337,7 @@ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ id: "airtable", group: "work", name: "Airtable", - description: "Query and update records, tables, and bases in Airtable.", + descriptionKey: "pluginsPage.connectorDescriptions.airtable", action: { kind: "mcp", mcp: { @@ -357,14 +356,14 @@ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ id: "jira", group: "work", name: "Jira", - description: "Create, search, and triage Jira tickets from chat.", + descriptionKey: "pluginsPage.connectorDescriptions.jira", action: { kind: "clawhub", query: "jira" }, }, { id: "canva", group: "work", name: "Canva", - description: "Create and edit Canva designs, manage assets, and export results.", + descriptionKey: "pluginsPage.connectorDescriptions.canva", action: { kind: "mcp", mcp: { @@ -379,7 +378,7 @@ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ id: "stripe", group: "work", name: "Stripe", - description: "Check payments, customers, invoices, and subscriptions in your Stripe account.", + descriptionKey: "pluginsPage.connectorDescriptions.stripe", action: { kind: "mcp", mcp: { @@ -394,28 +393,28 @@ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ id: "google-calendar", group: "work", name: "Calendar", - description: "Read, create, and get briefed on events — your agent owns your schedule.", + descriptionKey: "pluginsPage.connectorDescriptions.googleCalendar", action: { kind: "clawhub", query: "google calendar" }, }, { id: "email-inbox", group: "work", name: "Email", - description: "Mailbox triage, summaries, and drafts with send-on-approval.", + descriptionKey: "pluginsPage.connectorDescriptions.email", action: { kind: "clawhub", query: "email" }, }, { id: "pdf-tools", group: "work", name: "PDF", - description: "Extract, merge, convert, and OCR PDF documents.", + descriptionKey: "pluginsPage.connectorDescriptions.pdf", action: { kind: "clawhub", query: "pdf" }, }, { id: "transcription", group: "work", name: "Transcription", - description: "Turn audio and video into clean, structured transcripts.", + descriptionKey: "pluginsPage.connectorDescriptions.transcription", action: { kind: "clawhub", query: "transcription" }, }, // --- Coding & infrastructure --- @@ -423,7 +422,7 @@ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ id: "github", group: "dev", name: "GitHub", - description: "PR review queues, issue triage, and repo Q&A through the official GitHub MCP.", + descriptionKey: "pluginsPage.connectorDescriptions.github", action: { kind: "mcp", mcp: { @@ -444,7 +443,7 @@ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ id: "sentry", group: "dev", name: "Sentry", - description: "Crash alerts explained and triaged the moment they fire.", + descriptionKey: "pluginsPage.connectorDescriptions.sentry", action: { kind: "mcp", mcp: { @@ -459,7 +458,7 @@ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ id: "context7", group: "dev", name: "Context7", - description: "Version-specific library docs and code examples while coding. No signup needed.", + descriptionKey: "pluginsPage.connectorDescriptions.context7", action: { kind: "mcp", mcp: { @@ -474,7 +473,7 @@ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ id: "deepwiki", group: "dev", name: "DeepWiki", - description: "Ask questions about any public GitHub repo. Free, no account needed.", + descriptionKey: "pluginsPage.connectorDescriptions.deepwiki", action: { kind: "mcp", mcp: { @@ -489,7 +488,7 @@ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ id: "hugging-face", group: "dev", name: "Hugging Face", - description: "Search models, datasets, and papers; run Spaces as tools.", + descriptionKey: "pluginsPage.connectorDescriptions.huggingFace", action: { kind: "mcp", mcp: { @@ -504,14 +503,14 @@ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ id: "grafana", group: "dev", name: "Grafana", - description: "Grafana know-how and community connectors for dashboards and alerts.", + descriptionKey: "pluginsPage.connectorDescriptions.grafana", action: { kind: "clawhub", query: "grafana" }, }, { id: "kubernetes", group: "dev", name: "Kubernetes", - description: "Cluster operations and troubleshooting from chat.", + descriptionKey: "pluginsPage.connectorDescriptions.kubernetes", action: { kind: "clawhub", query: "kubernetes" }, }, // --- Home & media --- @@ -519,7 +518,7 @@ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ id: "home-assistant", group: "home", name: "Home Assistant", - description: "Control lights, climate, and automations across your whole home.", + descriptionKey: "pluginsPage.connectorDescriptions.homeAssistant", action: { kind: "mcp", mcp: { @@ -537,21 +536,21 @@ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ id: "spotify", group: "home", name: "Spotify", - description: "Search, queue, and soundtrack your day with mood-based playlists.", + descriptionKey: "pluginsPage.connectorDescriptions.spotify", action: { kind: "clawhub", query: "spotify" }, }, { id: "sonos", group: "home", name: "Sonos", - description: "Whole-home audio: play, group rooms, and queue by chat.", + descriptionKey: "pluginsPage.connectorDescriptions.sonos", action: { kind: "clawhub", query: "sonos" }, }, { id: "reddit", group: "life", name: "Reddit", - description: "Browse, search, and summarize subreddits and threads.", + descriptionKey: "pluginsPage.connectorDescriptions.reddit", action: { kind: "clawhub", query: "reddit" }, }, // --- Everyday life --- @@ -559,42 +558,42 @@ export const CONNECTOR_SUGGESTIONS: readonly ConnectorSuggestion[] = [ id: "portfolio-pulse", group: "life", name: "Markets", - description: "Live stocks and crypto with price alerts and daily digests.", + descriptionKey: "pluginsPage.connectorDescriptions.markets", action: { kind: "clawhub", query: "stocks" }, }, { id: "trip-scout", group: "life", name: "Travel", - description: "Flight and hotel search with fare watching and trip memory.", + descriptionKey: "pluginsPage.connectorDescriptions.travel", action: { kind: "clawhub", query: "flights" }, }, { id: "morning-brief", group: "life", name: "News", - description: "A personalized daily briefing: news, weather, and tasks in one message.", + descriptionKey: "pluginsPage.connectorDescriptions.news", action: { kind: "clawhub", query: "news" }, }, { id: "maps", group: "life", name: "Maps", - description: "Places, routing, and travel-time answers.", + descriptionKey: "pluginsPage.connectorDescriptions.maps", action: { kind: "clawhub", query: "maps" }, }, { id: "translation", group: "life", name: "Translation", - description: "Translate and localize text and documents.", + descriptionKey: "pluginsPage.connectorDescriptions.translation", action: { kind: "clawhub", query: "translation" }, }, { id: "notes", group: "life", name: "Notes", - description: "Capture notes to Markdown, Obsidian, Notion, or Bear.", + descriptionKey: "pluginsPage.connectorDescriptions.notes", action: { kind: "clawhub", query: "notes" }, }, ]; diff --git a/ui/src/pages/plugins/view.ts b/ui/src/pages/plugins/view.ts index b5a2b33b520c..55420a197adc 100644 --- a/ui/src/pages/plugins/view.ts +++ b/ui/src/pages/plugins/view.ts @@ -158,7 +158,7 @@ function matchesConnector(connector: ConnectorSuggestion, query: string): boolea if (!needle) { return true; } - return [connector.id, connector.name, connector.description].some((value) => + return [connector.id, connector.name, t(connector.descriptionKey)].some((value) => value.toLocaleLowerCase().includes(needle), ); } @@ -751,8 +751,10 @@ function renderMcpRow(server: McpServerSummary, props: PluginsViewProps): Templa

${server.name}

- MCP - ${server.auth === "oauth" ? html`OAuth` : nothing} + ${t("pluginsPage.mcp")} + ${server.auth === "oauth" + ? html`${t("pluginsPage.oauth")}` + : nothing}

${server.target}

${server.transport}
@@ -921,10 +923,10 @@ function renderConnectorCard(

${connector.name}

-

${connector.description}

+

${t(connector.descriptionKey)}

${isMcp - ? html`MCP + ? html`${t("pluginsPage.mcp")} ${t("pluginsPage.connectorMcpNote")}` : html`${t("pluginsPage.connectorClawHubNote")}`}
diff --git a/ui/src/pages/skill-workshop/skill-workshop-page.ts b/ui/src/pages/skill-workshop/skill-workshop-page.ts index 4e1c45f9507e..7db86699a5b5 100644 --- a/ui/src/pages/skill-workshop/skill-workshop-page.ts +++ b/ui/src/pages/skill-workshop/skill-workshop-page.ts @@ -167,7 +167,7 @@ function renderSkillWorkshopHeaderControls(state: SkillWorkshopState, requestUpd
diff --git a/ui/src/pages/skill-workshop/view.ts b/ui/src/pages/skill-workshop/view.ts index 5709ee1d91c5..3057e589fc01 100644 --- a/ui/src/pages/skill-workshop/view.ts +++ b/ui/src/pages/skill-workshop/view.ts @@ -65,21 +65,21 @@ const STATUS_TABS: SkillWorkshopStatusFilter[] = [ ]; const STATUS_LABEL: Record = { - all: "All", - pending: "Pending", - applied: "Applied", - rejected: "Rejected", - quarantined: "Quarantined", - stale: "Stale", + all: "skillWorkshop.status.all", + pending: "skillWorkshop.status.pending", + applied: "skillWorkshop.status.applied", + rejected: "skillWorkshop.status.rejected", + quarantined: "skillWorkshop.status.quarantined", + stale: "skillWorkshop.status.stale", }; const TODAY_PREVIEW_MAX_ITEMS = 3; const TODAY_PREVIEW_MAX_ITEM_CHARS = 120; const GROUP_LABEL: Record = { - today: "Today", - yesterday: "Yesterday", - earlier: "Earlier this week", + today: "skillWorkshop.recency.today", + yesterday: "skillWorkshop.recency.yesterday", + earlier: "skillWorkshop.recency.earlier", }; export function renderSkillWorkshop(props: SkillWorkshopProps) { @@ -123,7 +123,7 @@ export function renderSkillWorkshop(props: SkillWorkshopProps) { .files=${selected.supportFiles} .activePath=${preview.path} .query=${props.filePreviewQuery} - .contextLabel=${`in ${selected.slug}`} + .contextLabel=${t("skillWorkshop.previewContext", { slug: selected.slug })} @file-preview-query-change=${(event: CustomEvent) => props.onFilePreviewQueryChange(event.detail)} @file-preview-select=${(event: CustomEvent) => @@ -139,7 +139,8 @@ export function renderSkillWorkshop(props: SkillWorkshopProps) { function renderRevisionDialog(props: SkillWorkshopProps, proposal: SkillWorkshopProposal) { const busy = props.actionBusy?.key === proposal.key && props.actionBusy.action === "revise"; const canSubmit = props.revisionDraft.trim().length > 0 && !props.actionBusy; - const verb = props.mode === "board" ? "Revise" : "Tweak"; + const verb = + props.mode === "board" ? t("skillWorkshop.actions.revise") : t("skillWorkshop.actions.tweak"); return html`