From 45be429b3bc41e0ebd3d7129b70fd293b1dd6b36 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 19 Jul 2026 00:03:39 -0400 Subject: [PATCH] docs(desktop): document plugin ctx.i18n in the plugins skill Add the ctx.i18n.register / usePluginI18n surface to the desktop-plugins skill and a bilingual (en/ja) example to the starter template. --- skills/hermes-desktop-plugins/SKILL.md | 6 ++++ .../templates/plugin.js | 36 +++++++++++++++---- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/skills/hermes-desktop-plugins/SKILL.md b/skills/hermes-desktop-plugins/SKILL.md index 71af411b7..93b985396 100644 --- a/skills/hermes-desktop-plugins/SKILL.md +++ b/skills/hermes-desktop-plugins/SKILL.md @@ -83,6 +83,12 @@ The ONLY import surface is `@hermes/plugin-sdk` (plus `react` / (renders below Artifacts, lights up at the route) — and/or a `PALETTE_AREA` command calling `host.navigate('/my-page')`. - `ctx.storage.get/set/remove` — persistence namespaced to your plugin. +- `ctx.i18n.register({ en, ja, ... })` — ship your OWN locale bundles, scoped + to your plugin (never edit core `en.ts`). Values are literal strings or + interpolator functions; nested trees are addressed by dot-path. Read them + reactively in components with `usePluginI18n(id)` returning `t('key', ...args)` + (re-renders on a locale switch), or via `ctx.i18n.t` in handlers/stores. + Resolution follows the app's active locale, then your `en`, then the raw key. - Data: `useQuery`/`useMutation`/`useQueryClient`/`queryClient` (the app's ONE React Query client — cache, dedupe, `refetchInterval`, invalidate like core; never hand-roll a poll loop), plus `atom`/`computed` for plugin-local state. diff --git a/skills/hermes-desktop-plugins/templates/plugin.js b/skills/hermes-desktop-plugins/templates/plugin.js index 803c11d2d..e3e75ed47 100644 --- a/skills/hermes-desktop-plugins/templates/plugin.js +++ b/skills/hermes-desktop-plugins/templates/plugin.js @@ -10,27 +10,34 @@ * Only these imports resolve: @hermes/plugin-sdk, react, react/jsx-runtime. */ -import { cn, haptic, host, Tip, useValue } from '@hermes/plugin-sdk' +import { cn, haptic, host, Tip, usePluginI18n, useValue } from '@hermes/plugin-sdk' import { jsx, jsxs } from 'react/jsx-runtime' +// Ship your OWN strings (never edit core en.ts). `usePluginI18n` resolves them +// against the app's active locale, falling back to `en`, then the raw key. +const ID = 'my-plugin' + function MyPane() { + const t = usePluginI18n(ID) const gateway = useValue(host.state.gateway) return jsxs('div', { className: 'flex h-full flex-col gap-2 p-3 text-sm', children: [ - jsx('div', { className: 'font-medium', children: 'My Plugin Pane' }), + jsx('div', { className: 'font-medium', children: t('paneTitle') }), jsx('div', { className: 'text-(--ui-text-tertiary)', - children: `gateway: ${gateway}` + children: t('gateway', gateway) }) ] }) } function MyChip() { + const t = usePluginI18n(ID) + return jsx(Tip, { - label: 'My plugin — click me', + label: t('chipTip'), children: jsx('button', { className: cn( 'inline-flex h-full items-center gap-1 px-1.5 text-[0.6875rem] transition-colors', @@ -39,7 +46,7 @@ function MyChip() { type: 'button', onClick: () => { haptic('tap') - host.notify({ kind: 'info', message: 'Hello from my plugin!' }) + host.notify({ kind: 'info', message: t('hello') }) }, children: 'my-plugin' }) @@ -47,9 +54,26 @@ function MyChip() { } export default { - id: 'my-plugin', // must match the folder name + id: ID, // must match the folder name name: 'My Plugin', register(ctx) { + // Register locale bundles — scoped to this plugin, torn down on reload. + // Values are literals or interpolators (`arg => `…${arg}…``). + ctx.i18n.register({ + en: { + paneTitle: 'My Plugin Pane', + gateway: state => `gateway: ${state}`, + chipTip: 'My plugin — click me', + hello: 'Hello from my plugin!' + }, + ja: { + paneTitle: 'マイプラグイン', + gateway: state => `ゲートウェイ: ${state}`, + chipTip: 'マイプラグイン — クリック', + hello: 'マイプラグインからこんにちは!' + } + }) + // A layout pane — auto-placed by the placement hint; user can drag it. // To land on a specific edge instead of stacking, add a dock gesture, // e.g. below the conversation: