Skip to content

feat: add LANGUAGE option for English bot replies - #12

Open
jonathanherr wants to merge 1 commit into
leecoder:mainfrom
jonathanherr:feat/english-locale-option
Open

jonathanherr wants to merge 1 commit into
leecoder:mainfrom
jonathanherr:feat/english-locale-option

Conversation

@jonathanherr

@jonathanherr jonathanherr commented Sep 17, 2026

Copy link
Copy Markdown

All user-facing Slack messages (commands, question prompts, permission requests, bg-task updates) are hardcoded in Korean, so English-speaking workspaces see mixed-language replies that no model instruction can fix (verified: strings live in src/plugin.ts, not in any LLM prompt).

This PR adds a LANGUAGE plugin option ('ko' default, 'en' opt-in; also readable from SLACK_LANGUAGE env) backed by a ko/en string table + t() helper, following the same option-resolution pattern as DEFAULT_AGENT/ATTACH_TIMEOUT_SEC. Default stays ko, so existing behavior is unchanged unless opted in.

[["opencode-slack-agent", {
  "SLACK_BOT_TOKEN": "xoxb-...",
  "SLACK_APP_TOKEN": "xapp-...",
  "LANGUAGE": "en"
}]]

Covers ~55 user-facing strings: !model/!agent/!dir/!attach/!sync/!reset/!help responses, question forwarding, permission requests/replies, bg-task updates, timeout labels. Tool description made bilingual (evaluated at module load, before options resolve). README options table updated.

Verification: npm run build succeeds; built output contains both locales + resolver. Note: npx tsc --noEmit reports one error at src/plugin.ts:373 (Property 'completed' does not exist...) which is pre-existing on the base commit, untouched by this change.


Summary by cubic

Adds a LANGUAGE plugin option so all user-facing Slack replies (commands, question prompts, permission requests, bg-task updates) can be English instead of hardcoded Korean. Default stays ko, and the option is also readable from SLACK_LANGUAGE.

Bug Fixes

  • Sync cursor now falls back to m.info?.id and resets when a thread switches sessions, so messages aren't skipped or replayed.
  • Thread replies without a linked session are ignored unless the bot is mentioned, the sender is allowlisted, or it's a DM.
  • Server init failure cleans up worker state and rethrows instead of leaving the plugin half-initialized.

Written for commit b17353c. Summary will update on new commits.

Review in cubic

All user-facing Slack messages (commands, question prompts,
permission requests, bg-task updates) were hardcoded in Korean.
Adds a LANGUAGE plugin option ('ko' default, 'en' opt-in, also
readable from SLACK_LANGUAGE) backed by a ko/en string table,
so English-speaking workspaces get English replies without
changing default behavior.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/plugin.ts">

<violation number="1" location="src/plugin.ts:109">
P2: When a substituted value contains `$&`, `t()` interprets it as a replacement token and corrupts the Slack reply. Use the replacer-function overload so variable values are inserted literally.</violation>

<violation number="2" location="src/plugin.ts:1091">
P3: When `LANGUAGE=ko` and no model override is set, `!model` still displays the English `(default)` label. Use the localized `defaultLabel` for the fallback value, as `!agent` already does.</violation>
</file>

Shadow auto-approve: would not auto-approve because issues were found.

Re-trigger cubic

Comment thread src/plugin.ts
function t(key: keyof typeof STR, vars?: Record<string, string | number>): string {
let s = STR[key][language];
if (vars) {
for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, String(v));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a substituted value contains $&, t() interprets it as a replacement token and corrupts the Slack reply. Use the replacer-function overload so variable values are inserted literally.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/plugin.ts, line 109:

<comment>When a substituted value contains `$&`, `t()` interprets it as a replacement token and corrupts the Slack reply. Use the replacer-function overload so variable values are inserted literally.</comment>

<file context>
@@ -29,6 +29,87 @@ let defaultDirectory: string = "";
+function t(key: keyof typeof STR, vars?: Record<string, string | number>): string {
+  let s = STR[key][language];
+  if (vars) {
+    for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, String(v));
+  }
+  return s;
</file context>
Suggested change
for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, String(v));
for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, () => String(v));

Comment thread src/plugin.ts
? `${modelOverride.providerID}/${modelOverride.modelID}`
: "(default)";
const lines = [`*현재 모델:* \`${current}\``, "", "*사용 가능:*"];
const lines = [t("currentModel", { v: current }), "", t("available")];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When LANGUAGE=ko and no model override is set, !model still displays the English (default) label. Use the localized defaultLabel for the fallback value, as !agent already does.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/plugin.ts, line 1091:

<comment>When `LANGUAGE=ko` and no model override is set, `!model` still displays the English `(default)` label. Use the localized `defaultLabel` for the fallback value, as `!agent` already does.</comment>

<file context>
@@ -1007,7 +1088,7 @@ async function handleCommand(channel: string, text: string, ts: string): Promise
         ? `${modelOverride.providerID}/${modelOverride.modelID}`
         : "(default)";
-      const lines = [`*현재 모델:* \`${current}\``, "", "*사용 가능:*"];
+      const lines = [t("currentModel", { v: current }), "", t("available")];
       try {
         const { data: config } = await pluginClient.config.get();
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant