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 +3 -1
- package/package.json +1 -1
- package/src/activation/question-hooks.ts +10 -9
- package/src/question-tool.ts +43 -12
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
|

|
|
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": "
|
|
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
|
@@ -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, {
|
|
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
|
-
|
|
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:
|
|
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) {
|
package/src/question-tool.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|