Skip to content

feat(desktop): introduce plugin i18n for Bot Mode (en bundle, no behavior change) - #91489

Closed
Finn763 wants to merge 2 commits into
NousResearch:mainfrom
Finn763:feat/desktop-botmode-i18n-en
Closed

Finn763 wants to merge 2 commits into
NousResearch:mainfrom
Finn763:feat/desktop-botmode-i18n-en

Conversation

@Finn763

@Finn763 Finn763 commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Context

The hermes-bots plugin (Bot Mode) hardcodes every user-visible string, which blocks the zh-CN localization requested in #91336. This PR lays the i18n plumbing; locale bundles land in follow-up PRs once the plumbing is reviewed.

Approach

Mirrors the kanban plugin's i18n pattern (#67303), with one deliberate difference — the en template lives inline in plugin.js:

  • plugin.js is the single-file bundle: sibling imports break the direct-file plugin loader, and the 57-file .mjs test harness evaluates plugin.js alone in a vm sandbox.
  • BOTS_EN message template + BOTS_LOCALES = { en: BOTS_EN } bundle, defined inline.
  • ctx.i18n.register(BOTS_LOCALES) at load, guarded — hosts predating plugin i18n (and the test sandbox, which strips the SDK import) don't crash.
  • useBots() binds the en template through sdk.usePluginI18n('hermes-bots') when available; otherwise it returns the en template verbatim, so every rendered string is byte-identical to the hardcoded literal it replaced.
  • Non-component code (the /new→/compact composer middleware in register(ctx)) uses ctx.i18n.t with the same en-template fallback.

Surfaces rewired

  • Bots pane header (title, activity-toast toggle, hidden-bot eye toggle, New dropdown, pin/unpin tips, gateway-waiting state, empty state)
  • New Agent dialog — fields, placeholders, name-collision notices, Create-on picker, Advanced disclosure + all four tabs (General/Capabilities/Skills/Tools/MCP), clone options, SOUL.md block, share-keys option, empty-skills note
  • Edit Profile dialog + Advanced configuration (Skills Hub, MCP empty state, hub search)
  • Bot context menu (Hide/Unhide, Edit Profile, Groups, Duplicate, New chat, Delete) + delete confirmation
  • Composer middleware /new→/compact reroute notice

Verification

  • npm run typecheck (tsc -p ., tsconfig.electron.json, tsconfig.e2e.json) — all green
  • node --test apps/desktop/src/plugins/hermes-bots/tests/*.test.mjs — 376/376 pass (5 source-shape assertions updated in this PR to match the new bundle-lookup form; behavior tests get a useBots stub since they never assert label text)

Follow-up

  • zh-CN bundle (PR 2), covering the same surfaces — native speaker review included.

Refs #91336


Rebase note

Rebased onto main @ 2026-08-23T23:34Z 解决合并冲突(冲突文件:apps/desktop/src/plugins/hermes-bots/plugin.js、apps/desktop/src/plugins/hermes-bots/tests/active-now-strip.test.mjs、apps/desktop/src/plugins/hermes-bots/tests/bot-delete.test.mjs、apps/desktop/src/plugins/hermes-bots/tests/hide-bots.test.mjs、apps/desktop/src/plugins/hermes-bots/tests/bots-search.test.mjs)。无行为变化。

Resolution notes for reviewers:

  • Kept main's newer behavior everywhere (openRosterBot open path, async ensureBotMetadata context-menu actions, roster toolbar with gateway/activity filters, Hidden-section model); the i18n lookups were carried onto those structures.
  • en bundle values updated where main renamed copy (New Bot / Create Bot / No bots yet / Create your first bot. / New bot or group chat / Hide / Unhide / Search bots and group chats…) so renders stay byte-identical to current main.
  • pinStayHint bundle key removed — main deleted the remote-row "stay pinned" toast, so the key had no call sites left.
  • Adapted tests: bots-home.test.mjs create-button shape assertion → k.create.creating/k.create.createAgent; profile-prewarm.test.mjs i18n stub lint fix; plugin-i18n.test.mjs expectations updated for the renamed copy.
  • Verification on the rebased tree: npm run typecheck green (tsc -p ., tsconfig.electron.json, tsconfig.e2e.json); node --test src/plugins/hermes-bots/tests/*.test.mjs 530/530 pass; vitest (dock-enforce, hide-only-strip-tabs, runtime-loader) 24/24 pass; eslint on hermes-bots clean.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/desktop Electron desktop app (apps/desktop/*) comp/plugins Plugin system and bundled plugins area/i18n Localization, locales, translations labels Aug 21, 2026
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference; please use your judgment.

  1. apps/desktop/src/plugins/hermes-bots/plugin.js (multiple sites) — The sweep is incomplete, and one spot is internally inconsistent: the pin/unpin toast is translated (k.pane.pinToggle) but the menu item right next to it still hardcodes children: meta?.pinned ? 'Unpin' : 'Pin to top'; other survivors include description: 'Create your first teammate.', doneLabel: 'Deleted', host.notifyError(err, \Could not clean up draft profile "${draft}"`), the roster-unavailable gateway error line, the 'Search bots…'placeholder, and the plugin's exporteddescription` metadata. Why it matters: a future non-en bundle produces a mixed-language UI where sibling controls flip languages mid-sentence. Suggestion: either finish these (they all fit the existing pane/menu/common groups) or add a TODO list documenting them as out of scope for this first bundle.

  2. plugin.js:~168 (bindBotsText / BOTS_LOCALES) — The registered bundle contains function-valued entries (showHiddenBots: n => …, pinToggle: (name, pinned) => …), and the client side assumes t(path, ...args) accepts positional interpolation args. Why it matters: if the host-side plugin-i18n contract (feat(desktop): plugin-scoped i18n — ctx.i18n locale bundles (follow-up to #60638) #67303) expects flat string templates with named placeholders (the common ICU/{n} style) and ignores or chokes on function values, every parameterized string breaks only on hosts WITH the feature — exactly where the graceful-degradation fallback doesn't protect. Suggestion: assert the contract in a test (register a stub host whose t receives args and returns composed output), or normalize the bundle to placeholder strings before ctx.i18n.register.

  3. plugin.js:~160 (useBots) — Nit: when the SDK translator exists, bindBotsText(usePluginI18n('hermes-bots'), BOTS_EN) rebuilds the whole wrapper tree (new function identities) on every render of every Bot component. Why it matters: harmless today (labels aren't effect deps), but any future useEffect(..., [k.pane.pinToggle])) pattern would loop. Suggestion: useMemo` the bound object keyed by the translator identity.

  4. tests/* — Positive: updating the source-structure assertions to match the k.* indirection while stubbing useBots with a self-returning Proxy keeps the behavior tests honest without coupling them to label text; the rename of the shadowing ([k]) destructure to ([x]) in EditProfileDialog was caught correctly. No change requested.

@Finn763

Finn763 commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the automated review findings (a92c175b):

  1. Finished the sweep: pin/unpin menu items, 'Create your first teammate.', 'Deleted', draft-cleanup failure template, roster-unavailable error, 'Search bots…' placeholder, and plugin export metadata all now source from BOTS_EN. Remaining out-of-scope surfaces are documented in a TODO(i18n sweep) list next to BOTS_LOCALES.
  2. Contract verified against feat(desktop): plugin-scoped i18n — ctx.i18n locale bundles (follow-up to #60638) #67303 and the SDK: PluginMessageValue = string | ((...args) => string) — function-valued leaves ARE the contract (kanban uses the same pattern), so no placeholder normalization needed. Locked it with 4 new contract tests in plugin-i18n.test.mjs (positional-arg forwarding, byte-identity with/without host i18n, useBots memoization).
  3. useBots() now memoizes the bound tree on translator identity (mirrors kanban's useKanban).

Tests: 380/380 green (376 baseline + 4 new).

…vior change)

hermes-bots hardcodes every user-visible string, which blocks the
zh-CN localization requested in NousResearch#91336. This commit lays the plumbing,
mirroring the kanban plugin's pattern:

- Inline BOTS_EN message template + BOTS_LOCALES bundle (plugin.js is
  the single-file bundle — sibling imports break the direct-file plugin
  loader and the .mjs test harness, so the en template lives inline).
- ctx.i18n.register(BOTS_LOCALES) at load, guarded for pre-NousResearch#67303 hosts
  that lack plugin i18n.
- useBots() binds the en template through sdk.usePluginI18n when
  available and falls back to the en template verbatim otherwise, so
  every rendered string is byte-identical to what it replaced.
- Rewires the issue's named surfaces — Bots pane header, New Agent
  dialog (incl. Advanced tabs), Edit Profile dialog, bot context menu,
  delete confirmation, and the /new->/compact composer notice — from
  hardcoded literals to bundle lookups.

Follow-up PRs add the zh locale bundle (and other languages) once this
plumbing is reviewed.

Refs NousResearch#91336
…contract

Addresses review feedback on NousResearch#91489:

- Move the surviving hardcoded strings into BOTS_EN — pin/unpin context
  menu item (menu.pin/menu.unpin), empty-state description, delete
  doneLabel, draft-cleanup failure, roster-unavailable error, roster
  search field (aria + placeholder), and the static export name/
  description (sourced from the bundle; static metadata has no
  translator, noted in a comment). Renders stay byte-identical.
- Document the remaining out-of-scope surfaces in a TODO list beside
  BOTS_LOCALES (group chat UI, cron UI, avatar editor, prompts, etc.).
- useBots(): memoize the bound tree on the translator identity
  (useMemo(..., [t]), mirroring kanban's useKanban) so components no
  longer rebuild function identities on every render.
- Add plugin-i18n.test.mjs contract tests: function-valued bundle leaves
  + positional args are the documented contract (NousResearch#67303 body and
  apps/desktop/src/i18n/plugin-i18n.ts PluginMessageValue/translateFrom),
  verified end-to-end on a stub host whose t receives args; verbatim
  en-template fallback without the feature; useBots memoization.
@teknium1

Copy link
Copy Markdown
Collaborator

Thanks @Finn763 — this landed on main through #113430 (fix(bot-mode): roster menu and group row speak the active language (#91667, #108978, salva), merged as 4d46d76. Your commits/analysis were carried in with credit (see that PR's body and Co-authored-by trailers); the salvage rebased onto current main, widened to sibling surfaces where the review found gaps, and was live-verified in the real Electron app. Closing this one as superseded — thank you for the fix.

@teknium1 teknium1 closed this Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/i18n Localization, locales, translations comp/desktop Electron desktop app (apps/desktop/*) comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants