opencode-translate 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # opencode-translate
2
2
 
3
+ > Use opencode in your native language. LLM always hears English.
4
+
3
5
  ![banner](https://github.com/user-attachments/assets/0bb6739c-abc5-4c3e-837e-2aaf5533a359)
4
6
 
5
7
  ## Demo
@@ -32,7 +34,7 @@ Add to `~/.config/opencode/opencode.jsonc`:
32
34
  {
33
35
  "plugin": [
34
36
  ["opencode-translate", {
35
- "model": "anthropic/claude-haiku-4-5", // model to use for translation
37
+ "model": "openai/gpt-5.4-mini", // model to use for translation
36
38
  "lang": "Korean" // language you speak
37
39
  }]
38
40
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-translate",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "OpenCode plugin that lets the user chat in a configured language while the main chat loop only sees English.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -4,6 +4,7 @@ import {
4
4
  isQuestionArgs,
5
5
  type QuestionSnapshot,
6
6
  type QuestionToolOutput,
7
+ restoreQuestionArgs,
7
8
  restoreQuestionOutput,
8
9
  snapshotQuestions,
9
10
  translateQuestionArgs,
@@ -58,7 +59,11 @@ export function createToolExecuteBeforeHook(ctx: HookContext): NonNullable<Hooks
58
59
  }
59
60
  }
60
61
 
61
- questionSnapshots.set(input.callID, { original, translated: snapshotQuestions(args) })
62
+ questionSnapshots.set(input.callID, {
63
+ original,
64
+ translated: snapshotQuestions(args),
65
+ userLanguage: activeState.translate_user_lang,
66
+ })
62
67
  pruneQuestionSnapshots()
63
68
  } catch (error) {
64
69
  await logError(ctx.client, error)
@@ -73,10 +78,9 @@ export function createToolExecuteAfterHook(ctx: HookContext): NonNullable<Hooks[
73
78
  const snapshot = questionSnapshots.get(input.callID)
74
79
  if (!snapshot) return
75
80
  questionSnapshots.delete(input.callID)
81
+ if (isQuestionArgs(input.args)) restoreQuestionArgs(input.args, snapshot.original)
76
82
 
77
- const resolved = await resolveSessionState(ctx.client, ctx.directory, input.sessionID)
78
- const activeState = resolved.state
79
- if (!activeState || activeState.translate_user_lang === LLM_LANGUAGE) {
83
+ if (snapshot.userLanguage === LLM_LANGUAGE) {
80
84
  await restoreQuestionOutput(output as QuestionToolOutput, snapshot)
81
85
  return
82
86
  }
@@ -85,15 +89,12 @@ export function createToolExecuteAfterHook(ctx: HookContext): NonNullable<Hooks[
85
89
  translateCustomAnswer: (text: string) =>
86
90
  ctx.translator.translateText({
87
91
  text,
88
- sourceLanguage: activeState.translate_user_lang,
92
+ sourceLanguage: snapshot.userLanguage,
89
93
  targetLanguage: LLM_LANGUAGE,
90
94
  direction: "inbound",
91
95
  }),
92
96
  onTranslationError: async (error) => {
93
- await logError(
94
- ctx.client,
95
- buildInboundTranslationError(activeState.translate_user_lang, normalizeReason(error)),
96
- )
97
+ await logError(ctx.client, buildInboundTranslationError(snapshot.userLanguage, normalizeReason(error)))
97
98
  },
98
99
  })
99
100
  } catch (error) {
@@ -29,6 +29,7 @@ export interface QuestionArgs {
29
29
  export interface QuestionSnapshot {
30
30
  original: TextRecord[]
31
31
  translated: TextRecord[]
32
+ userLanguage: string
32
33
  }
33
34
 
34
35
  export interface QuestionToolOutput {
@@ -56,6 +57,10 @@ export function snapshotQuestions(args: QuestionArgs): TextRecord[] {
56
57
  return args.questions.map(cloneQuestion)
57
58
  }
58
59
 
60
+ export function restoreQuestionArgs(args: QuestionArgs, original: readonly TextRecord[]): void {
61
+ args.questions.splice(0, args.questions.length, ...original.map(cloneQuestion))
62
+ }
63
+
59
64
  export function isQuestionArgs(value: unknown): value is QuestionArgs {
60
65
  if (!value || typeof value !== "object") return false
61
66
  const questions = (value as Record<string, unknown>).questions
@@ -142,31 +147,55 @@ async function restoreLabel(
142
147
  }
143
148
  }
144
149
 
145
- // Reconstruct the exact output string the question tool would have produced
146
- // if it had been called with the original English args. Mirrors the format
147
- // in `packages/opencode/src/tool/question.ts` (as of opencode 1.14.x).
148
- export async function buildRestoredOutput(
150
+ async function restoreQuestionAnswers(
149
151
  original: readonly TextRecord[],
150
152
  translated: readonly TextRecord[],
151
153
  answers: readonly (readonly string[])[],
152
154
  options: RestoreQuestionOutputOptions = {},
153
- ): Promise<string> {
154
- const formattedParts = await Promise.all(
155
+ ): Promise<string[][]> {
156
+ return Promise.all(
155
157
  original.map(async (q, i) => {
156
158
  const selected = answers[i] ?? []
157
159
  const translatedOptions = translated[i]?.options ?? []
158
160
  const originalOptions = q.options
159
- const restored = await Promise.all(
160
- selected.map((label) => restoreLabel(label, translatedOptions, originalOptions, options)),
161
- )
162
- const rendered = restored.length > 0 ? restored.join(", ") : "Unanswered"
163
- return `"${q.question}"="${rendered}"`
161
+ return Promise.all(selected.map((label) => restoreLabel(label, translatedOptions, originalOptions, options)))
164
162
  }),
165
163
  )
164
+ }
165
+
166
+ function formatRestoredOutput(original: readonly TextRecord[], answers: readonly (readonly string[])[]): string {
167
+ const formattedParts = original.map((q, i) => {
168
+ const restored = answers[i] ?? []
169
+ const rendered = restored.length > 0 ? restored.join(", ") : "Unanswered"
170
+ return `"${q.question}"="${rendered}"`
171
+ })
166
172
  const formatted = formattedParts.join(", ")
167
173
  return `User has answered your questions: ${formatted}. You can now continue with the user's answers in mind.`
168
174
  }
169
175
 
176
+ // Reconstruct the exact output string the question tool would have produced
177
+ // if it had been called with the original English args. Mirrors the format
178
+ // in `packages/opencode/src/tool/question.ts` (as of opencode 1.14.x).
179
+ export async function buildRestoredOutput(
180
+ original: readonly TextRecord[],
181
+ translated: readonly TextRecord[],
182
+ answers: readonly (readonly string[])[],
183
+ options: RestoreQuestionOutputOptions = {},
184
+ ): Promise<string> {
185
+ const restoredAnswers = await restoreQuestionAnswers(original, translated, answers, options)
186
+ return formatRestoredOutput(original, restoredAnswers)
187
+ }
188
+
189
+ function mutableMetadata(output: QuestionToolOutput): Record<string, unknown> {
190
+ if (output.metadata && typeof output.metadata === "object" && !Array.isArray(output.metadata)) {
191
+ return output.metadata as Record<string, unknown>
192
+ }
193
+
194
+ const metadata: Record<string, unknown> = {}
195
+ output.metadata = metadata
196
+ return metadata
197
+ }
198
+
170
199
  export async function restoreQuestionOutput(
171
200
  output: QuestionToolOutput,
172
201
  snapshot: QuestionSnapshot,
@@ -175,5 +204,7 @@ export async function restoreQuestionOutput(
175
204
  if (typeof output.output !== "string") return
176
205
  const answersRaw = (output.metadata as { answers?: readonly (readonly string[])[] } | undefined)?.answers
177
206
  const answers = Array.isArray(answersRaw) ? answersRaw : []
178
- output.output = await buildRestoredOutput(snapshot.original, snapshot.translated, answers, options)
207
+ const restoredAnswers = await restoreQuestionAnswers(snapshot.original, snapshot.translated, answers, options)
208
+ output.output = formatRestoredOutput(snapshot.original, restoredAnswers)
209
+ mutableMetadata(output).answers = restoredAnswers
179
210
  }