anomalyco / anomalyco/opencode

[FEATURE]: Plugin hook for localizing built-in TUI text (command titles and UI strings)

Open
#45,490 0 comments 1 reaction 2 assignees View on GitHub

@nexxeln is already working on this.

Since Aug 27, 2026.

2.0
Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Summary

A sanctioned way for TUI plugins to localize display text built into OpenCode — at minimum keymap command titles/descriptions/groups; ideally all user-facing UI strings.

This is not a generic "add i18n" request (see #37216, #39846, #43533 — clearly wanted). It targets the underlying plugin-API mechanism that would let the community build localization themselves, in any language, without a large upfront effort from the core team.

Proposed API (any one of these unblocks the use case)

  • context.keymap.transform((command) => partialMetadata) — invoked when catalog entries are read for display (palette / slash completion / help), letting plugins localize title, description, group.
  • Or context.keymap.subscribe((event) => …) — a "command registered" event so plugins can react instead of polling.
  • Longer term: a display-text broker such as context.ui.localize(text) => string consulted at render time, which would also cover non-command surfaces (settings dialog, footers, toasts).

What a localization plugin must do today, and why each piece hurts

I maintain opencode-i18n-v2 (npm), which translates the TUI command surface into zh-Hans / zh-Hant. The only viable mechanism in V2 is shadow commands: for every built-in command, register a same-id command in a global layer at high priority with a translated title and run: () => false, so execution chains down to the original. This workaround forces three fragile subsystems:

  1. Endless polling. Commands appear as routes and dialogs mount, and there is no registration event — so the plugin samples context.keymap.commands() on a timer (250 ms during startup, 5 s steady state) for the whole TUI lifetime.
  2. Echo detection. Later polls return the plugin's own translated shadows, so the plugin must recognize and exclude them, or it starts re-translating its own translations and pollutes its command registry.
  3. bind: false on every shadow. A named command whose bind is omitted automatically inherits its configured keybinding. In a global priority-100 layer, a shadow of session.child.first (default down) stole the down arrow from open dialogs: with the command palette open, pressing ↓ closed the palette and opened the subagent picker instead of moving the selection (dialog.select.next also binds down). Worth a documentation note even without the i18n context.

What does not work (verified)

The V1 approach — monkey-patching getCommands / getCommandEntries on the shared keymap object — is impossible in V2. Each plugin receives its own facade: context.keymap is extensible with own properties (layer, dispatch, shortcuts, commands, pending, active, mode), but experimentally patching commands() on one plugin's context is invisible to the host renderer and to other plugins.

Environment

  • opencode version: 0.0.0-beta-18314 (beta channel)
  • OS: Windows 11 (10.0.26200), x64
  • Affected surface: TUI plugin API (@opencode-ai/plugin, context.keymap)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.