opencode-input-translator 0.1.0 → 0.1.2
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/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +3 -4
- package/dist/index.d.mts +0 -7
- package/dist/index.mjs +0 -133
- package/dist/index.mjs.map +0 -1
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createOpenAICompatible as e}from"@ai-sdk/openai-compatible";import{Output as t,generateText as n}from"ai";import{z as r}from"zod";function i(e){if(!e||e.trim().length===0)return!0;let t=e.replace(/```[\s\S]*?```/g,` `);t=t.replace(/`[^`]*`/g,` `);let n=0,r=0;for(let e of t){let t=e.codePointAt(0);t!==void 0&&(t>=65&&t<=90||t>=97&&t<=122||t>=192&&t<=591?n++:(t>=19968&&t<=40959||t>=44032&&t<=55215||t>=1024&&t<=1279||t>=1536&&t<=1791||t>=3584&&t<=3711||t>=2304&&t<=2431)&&r++)}let i=n+r;return i===0?!0:n/i>=.7}function a(e){let t=[],n=0,r=e;return r=r.replace(/```[\s\S]*?```/g,e=>{let r=`__CODE_BLOCK_${n++}__`;return t.push({placeholder:r,original:e}),r}),r=r.replace(/`[^`]+`/g,e=>{let r=`__CODE_BLOCK_${n++}__`;return t.push({placeholder:r,original:e}),r}),{prose:r,blocks:t}}function o(e,t){let n=e;for(let e of t)n=n.replace(e.placeholder,e.original);return n}const s=r.object({translation:r.string().describe(`The English translation of the input text`)});async function c(r,i){try{let{output:a}=await n({model:e({name:`translator`,baseURL:`${i.baseUrl}/v1`,apiKey:i.apiKey,supportsStructuredOutputs:!0})(i.model),output:t.object({schema:s}),system:`Translate the following text to English. If the text is already in English, output it unchanged (as is).`,prompt:r,maxRetries:3,abortSignal:AbortSignal.timeout(10*1e3)});if(!a)throw Error(`No content in response`);return{translated:!0,text:a.translation}}catch(e){return console.warn(`[translateToEnglish] error:`,e),{translated:!1,text:r}}}const l=async({client:e})=>{let t=process.env.TRANSLATOR_API_KEY,n=process.env.TRANSLATOR_BASE_URL,r=process.env.TRANSLATOR_MODEL??`gpt-5-nano-2025-08-07`;if(!t||!n)return await e.app.log({body:{service:`input-translator`,level:`warn`,message:`Missing TRANSLATOR_API_KEY or TRANSLATOR_BASE_URL. Input Translator Plugin is disabled.`}}),{};let s={apiKey:t,baseUrl:n,model:r};return{"experimental.chat.messages.transform":async(e,t)=>{for(let e of t.messages)if(e.info.role===`user`)for(let t of e.parts){if(t.type!==`text`||t.text.trim().length===0||t.synthetic===!0||t.ignored===!0||t.metadata?.__translated===!0||i(t.text))continue;let{prose:e,blocks:n}=a(t.text);if(e.trim().length===0)continue;let r=await c(e,s);r.translated&&(t.text=o(r.text,n),t.metadata={...t.metadata,__translated:!0})}}}};export{l as InputTranslatorPlugin,l as default};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/detector.ts","../src/extractor.ts","../src/translator.ts","../src/index.ts"],"sourcesContent":["export function isEnglish(text: string): boolean {\n if (!text || text.trim().length === 0) return true;\n\n let prose = text.replace(/```[\\s\\S]*?```/g, ' ');\n prose = prose.replace(/`[^`]*`/g, ' ');\n\n let latinCount = 0;\n let nonLatinCount = 0;\n\n for (const ch of prose) {\n const cp = ch.codePointAt(0);\n if (cp === undefined) continue;\n\n if (\n (cp >= 0x0041 && cp <= 0x005a) || // A-Z\n (cp >= 0x0061 && cp <= 0x007a) || // a-z\n (cp >= 0x00c0 && cp <= 0x024f) // Latin Extended\n ) {\n latinCount++;\n } else if (\n (cp >= 0x4e00 && cp <= 0x9fff) || // CJK\n (cp >= 0xac00 && cp <= 0xd7af) || // Hangul\n (cp >= 0x0400 && cp <= 0x04ff) || // Cyrillic\n (cp >= 0x0600 && cp <= 0x06ff) || // Arabic\n (cp >= 0x0e00 && cp <= 0x0e7f) || // Thai\n (cp >= 0x0900 && cp <= 0x097f) // Devanagari\n ) {\n nonLatinCount++;\n }\n }\n\n const total = latinCount + nonLatinCount;\n if (total === 0) return true;\n\n return latinCount / total >= 0.7;\n}\n","import type { CodeBlock } from './types.ts';\n\nexport function extractCodeBlocks(text: string): {\n prose: string;\n blocks: CodeBlock[];\n} {\n const blocks: CodeBlock[] = [];\n let index = 0;\n let prose = text;\n\n // 1. Extract fenced code blocks (``` ... ```) first\n prose = prose.replace(/```[\\s\\S]*?```/g, (match) => {\n const placeholder = `__CODE_BLOCK_${index++}__`;\n blocks.push({ placeholder, original: match });\n return placeholder;\n });\n\n // 2. Extract inline code (` ... `)\n prose = prose.replace(/`[^`]+`/g, (match) => {\n const placeholder = `__CODE_BLOCK_${index++}__`;\n blocks.push({ placeholder, original: match });\n return placeholder;\n });\n\n return { prose, blocks };\n}\n\nexport function restoreCodeBlocks(text: string, blocks: CodeBlock[]): string {\n let result = text;\n for (const block of blocks) {\n result = result.replace(block.placeholder, block.original);\n }\n return result;\n}\n","import { createOpenAICompatible } from '@ai-sdk/openai-compatible';\nimport { generateText, Output } from 'ai';\nimport { z } from 'zod';\nimport type { TranslationResult, TranslatorConfig } from './types.ts';\n\nconst translationSchema = z.object({\n translation: z.string().describe('The English translation of the input text'),\n});\n\nexport async function translateToEnglish(\n text: string,\n config: TranslatorConfig,\n): Promise<TranslationResult> {\n try {\n const provider = createOpenAICompatible({\n name: 'translator',\n baseURL: `${config.baseUrl}/v1`,\n apiKey: config.apiKey,\n supportsStructuredOutputs: true,\n });\n\n const { output: result } = await generateText({\n model: provider(config.model),\n output: Output.object({\n schema: translationSchema,\n }),\n system:\n 'Translate the following text to English. If the text is already in English, output it unchanged (as is).',\n prompt: text,\n maxRetries: 3,\n abortSignal: AbortSignal.timeout(10 * 1000),\n });\n\n if (!result) throw new Error('No content in response');\n\n return { translated: true, text: result.translation };\n } catch (err) {\n console.warn('[translateToEnglish] error:', err);\n return { translated: false, text };\n }\n}\n","import type { Plugin } from '@opencode-ai/plugin';\nimport { isEnglish } from './detector.ts';\nimport { extractCodeBlocks, restoreCodeBlocks } from './extractor.ts';\nimport { translateToEnglish } from './translator.ts';\nimport type { TranslatorConfig } from './types.ts';\n\nexport const InputTranslatorPlugin: Plugin = async ({ client }) => {\n const apiKey = process.env.TRANSLATOR_API_KEY;\n const baseUrl = process.env.TRANSLATOR_BASE_URL;\n const model = process.env.TRANSLATOR_MODEL ?? 'gpt-5-nano-2025-08-07';\n\n if (!apiKey || !baseUrl) {\n await client.app.log({\n body: {\n service: 'input-translator',\n level: 'warn',\n message:\n 'Missing TRANSLATOR_API_KEY or TRANSLATOR_BASE_URL. Input Translator Plugin is disabled.',\n },\n });\n return {};\n }\n\n const config: TranslatorConfig = { apiKey, baseUrl, model };\n\n return {\n 'experimental.chat.messages.transform': async (_input, output) => {\n for (const message of output.messages) {\n if (message.info.role !== 'user') continue;\n\n for (const part of message.parts) {\n if (part.type !== 'text') continue;\n if (part.text.trim().length === 0) continue;\n if (part.synthetic === true || part.ignored === true) continue;\n if (part.metadata?.__translated === true) continue;\n if (isEnglish(part.text)) continue;\n\n const { prose, blocks } = extractCodeBlocks(part.text);\n if (prose.trim().length === 0) continue;\n\n const result = await translateToEnglish(prose, config);\n if (result.translated) {\n part.text = restoreCodeBlocks(result.text, blocks);\n part.metadata = { ...part.metadata, __translated: true };\n }\n }\n }\n },\n };\n};\n\nexport default InputTranslatorPlugin;\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-input-translator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "OpenCode plugin that translates non-English user input to English",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
6
|
"exports": {
|
|
8
|
-
".": "./dist/index.
|
|
7
|
+
".": "./dist/index.js",
|
|
9
8
|
"./package.json": "./package.json"
|
|
10
9
|
},
|
|
11
10
|
"files": [
|
|
@@ -16,7 +15,7 @@
|
|
|
16
15
|
"check:fix": "biome check --fix",
|
|
17
16
|
"typecheck": "tsc --noEmit",
|
|
18
17
|
"build": "bun run check && tsdown",
|
|
19
|
-
"local-deploy": "bun run build && cp dist/index.
|
|
18
|
+
"local-deploy": "bun run build && cp dist/index.js .opencode/plugins/opencode-input-translator.js",
|
|
20
19
|
"prepublishOnly": "bun run build"
|
|
21
20
|
},
|
|
22
21
|
"repository": {
|
package/dist/index.d.mts
DELETED
package/dist/index.mjs
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
2
|
-
import { Output, generateText } from "ai";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
|
|
5
|
-
//#region src/detector.ts
|
|
6
|
-
function isEnglish(text) {
|
|
7
|
-
if (!text || text.trim().length === 0) return true;
|
|
8
|
-
let prose = text.replace(/```[\s\S]*?```/g, " ");
|
|
9
|
-
prose = prose.replace(/`[^`]*`/g, " ");
|
|
10
|
-
let latinCount = 0;
|
|
11
|
-
let nonLatinCount = 0;
|
|
12
|
-
for (const ch of prose) {
|
|
13
|
-
const cp = ch.codePointAt(0);
|
|
14
|
-
if (cp === void 0) continue;
|
|
15
|
-
if (cp >= 65 && cp <= 90 || cp >= 97 && cp <= 122 || cp >= 192 && cp <= 591) latinCount++;
|
|
16
|
-
else if (cp >= 19968 && cp <= 40959 || cp >= 44032 && cp <= 55215 || cp >= 1024 && cp <= 1279 || cp >= 1536 && cp <= 1791 || cp >= 3584 && cp <= 3711 || cp >= 2304 && cp <= 2431) nonLatinCount++;
|
|
17
|
-
}
|
|
18
|
-
const total = latinCount + nonLatinCount;
|
|
19
|
-
if (total === 0) return true;
|
|
20
|
-
return latinCount / total >= .7;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
//#endregion
|
|
24
|
-
//#region src/extractor.ts
|
|
25
|
-
function extractCodeBlocks(text) {
|
|
26
|
-
const blocks = [];
|
|
27
|
-
let index = 0;
|
|
28
|
-
let prose = text;
|
|
29
|
-
prose = prose.replace(/```[\s\S]*?```/g, (match) => {
|
|
30
|
-
const placeholder = `__CODE_BLOCK_${index++}__`;
|
|
31
|
-
blocks.push({
|
|
32
|
-
placeholder,
|
|
33
|
-
original: match
|
|
34
|
-
});
|
|
35
|
-
return placeholder;
|
|
36
|
-
});
|
|
37
|
-
prose = prose.replace(/`[^`]+`/g, (match) => {
|
|
38
|
-
const placeholder = `__CODE_BLOCK_${index++}__`;
|
|
39
|
-
blocks.push({
|
|
40
|
-
placeholder,
|
|
41
|
-
original: match
|
|
42
|
-
});
|
|
43
|
-
return placeholder;
|
|
44
|
-
});
|
|
45
|
-
return {
|
|
46
|
-
prose,
|
|
47
|
-
blocks
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
function restoreCodeBlocks(text, blocks) {
|
|
51
|
-
let result = text;
|
|
52
|
-
for (const block of blocks) result = result.replace(block.placeholder, block.original);
|
|
53
|
-
return result;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
//#endregion
|
|
57
|
-
//#region src/translator.ts
|
|
58
|
-
const translationSchema = z.object({ translation: z.string().describe("The English translation of the input text") });
|
|
59
|
-
async function translateToEnglish(text, config) {
|
|
60
|
-
try {
|
|
61
|
-
const { output: result } = await generateText({
|
|
62
|
-
model: createOpenAICompatible({
|
|
63
|
-
name: "translator",
|
|
64
|
-
baseURL: `${config.baseUrl}/v1`,
|
|
65
|
-
apiKey: config.apiKey,
|
|
66
|
-
supportsStructuredOutputs: true
|
|
67
|
-
})(config.model),
|
|
68
|
-
output: Output.object({ schema: translationSchema }),
|
|
69
|
-
system: "Translate the following text to English. If the text is already in English, output it unchanged (as is).",
|
|
70
|
-
prompt: text,
|
|
71
|
-
maxRetries: 3,
|
|
72
|
-
abortSignal: AbortSignal.timeout(10 * 1e3)
|
|
73
|
-
});
|
|
74
|
-
if (!result) throw new Error("No content in response");
|
|
75
|
-
return {
|
|
76
|
-
translated: true,
|
|
77
|
-
text: result.translation
|
|
78
|
-
};
|
|
79
|
-
} catch (err) {
|
|
80
|
-
console.warn("[translateToEnglish] error:", err);
|
|
81
|
-
return {
|
|
82
|
-
translated: false,
|
|
83
|
-
text
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
//#endregion
|
|
89
|
-
//#region src/index.ts
|
|
90
|
-
const InputTranslatorPlugin = async ({ client }) => {
|
|
91
|
-
const apiKey = process.env.TRANSLATOR_API_KEY;
|
|
92
|
-
const baseUrl = process.env.TRANSLATOR_BASE_URL;
|
|
93
|
-
const model = process.env.TRANSLATOR_MODEL ?? "gpt-5-nano-2025-08-07";
|
|
94
|
-
if (!apiKey || !baseUrl) {
|
|
95
|
-
await client.app.log({ body: {
|
|
96
|
-
service: "input-translator",
|
|
97
|
-
level: "warn",
|
|
98
|
-
message: "Missing TRANSLATOR_API_KEY or TRANSLATOR_BASE_URL. Input Translator Plugin is disabled."
|
|
99
|
-
} });
|
|
100
|
-
return {};
|
|
101
|
-
}
|
|
102
|
-
const config = {
|
|
103
|
-
apiKey,
|
|
104
|
-
baseUrl,
|
|
105
|
-
model
|
|
106
|
-
};
|
|
107
|
-
return { "experimental.chat.messages.transform": async (_input, output) => {
|
|
108
|
-
for (const message of output.messages) {
|
|
109
|
-
if (message.info.role !== "user") continue;
|
|
110
|
-
for (const part of message.parts) {
|
|
111
|
-
if (part.type !== "text") continue;
|
|
112
|
-
if (part.text.trim().length === 0) continue;
|
|
113
|
-
if (part.synthetic === true || part.ignored === true) continue;
|
|
114
|
-
if (part.metadata?.__translated === true) continue;
|
|
115
|
-
if (isEnglish(part.text)) continue;
|
|
116
|
-
const { prose, blocks } = extractCodeBlocks(part.text);
|
|
117
|
-
if (prose.trim().length === 0) continue;
|
|
118
|
-
const result = await translateToEnglish(prose, config);
|
|
119
|
-
if (result.translated) {
|
|
120
|
-
part.text = restoreCodeBlocks(result.text, blocks);
|
|
121
|
-
part.metadata = {
|
|
122
|
-
...part.metadata,
|
|
123
|
-
__translated: true
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
} };
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
//#endregion
|
|
132
|
-
export { InputTranslatorPlugin, InputTranslatorPlugin as default };
|
|
133
|
-
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/detector.ts","../src/extractor.ts","../src/translator.ts","../src/index.ts"],"sourcesContent":["export function isEnglish(text: string): boolean {\n if (!text || text.trim().length === 0) return true;\n\n let prose = text.replace(/```[\\s\\S]*?```/g, ' ');\n prose = prose.replace(/`[^`]*`/g, ' ');\n\n let latinCount = 0;\n let nonLatinCount = 0;\n\n for (const ch of prose) {\n const cp = ch.codePointAt(0);\n if (cp === undefined) continue;\n\n if (\n (cp >= 0x0041 && cp <= 0x005a) || // A-Z\n (cp >= 0x0061 && cp <= 0x007a) || // a-z\n (cp >= 0x00c0 && cp <= 0x024f) // Latin Extended\n ) {\n latinCount++;\n } else if (\n (cp >= 0x4e00 && cp <= 0x9fff) || // CJK\n (cp >= 0xac00 && cp <= 0xd7af) || // Hangul\n (cp >= 0x0400 && cp <= 0x04ff) || // Cyrillic\n (cp >= 0x0600 && cp <= 0x06ff) || // Arabic\n (cp >= 0x0e00 && cp <= 0x0e7f) || // Thai\n (cp >= 0x0900 && cp <= 0x097f) // Devanagari\n ) {\n nonLatinCount++;\n }\n }\n\n const total = latinCount + nonLatinCount;\n if (total === 0) return true;\n\n return latinCount / total >= 0.7;\n}\n","import type { CodeBlock } from './types.ts';\n\nexport function extractCodeBlocks(text: string): {\n prose: string;\n blocks: CodeBlock[];\n} {\n const blocks: CodeBlock[] = [];\n let index = 0;\n let prose = text;\n\n // 1. Extract fenced code blocks (``` ... ```) first\n prose = prose.replace(/```[\\s\\S]*?```/g, (match) => {\n const placeholder = `__CODE_BLOCK_${index++}__`;\n blocks.push({ placeholder, original: match });\n return placeholder;\n });\n\n // 2. Extract inline code (` ... `)\n prose = prose.replace(/`[^`]+`/g, (match) => {\n const placeholder = `__CODE_BLOCK_${index++}__`;\n blocks.push({ placeholder, original: match });\n return placeholder;\n });\n\n return { prose, blocks };\n}\n\nexport function restoreCodeBlocks(text: string, blocks: CodeBlock[]): string {\n let result = text;\n for (const block of blocks) {\n result = result.replace(block.placeholder, block.original);\n }\n return result;\n}\n","import { createOpenAICompatible } from '@ai-sdk/openai-compatible';\nimport { generateText, Output } from 'ai';\nimport { z } from 'zod';\nimport type { TranslationResult, TranslatorConfig } from './types.ts';\n\nconst translationSchema = z.object({\n translation: z.string().describe('The English translation of the input text'),\n});\n\nexport async function translateToEnglish(\n text: string,\n config: TranslatorConfig,\n): Promise<TranslationResult> {\n try {\n const provider = createOpenAICompatible({\n name: 'translator',\n baseURL: `${config.baseUrl}/v1`,\n apiKey: config.apiKey,\n supportsStructuredOutputs: true,\n });\n\n const { output: result } = await generateText({\n model: provider(config.model),\n output: Output.object({\n schema: translationSchema,\n }),\n system:\n 'Translate the following text to English. If the text is already in English, output it unchanged (as is).',\n prompt: text,\n maxRetries: 3,\n abortSignal: AbortSignal.timeout(10 * 1000),\n });\n\n if (!result) throw new Error('No content in response');\n\n return { translated: true, text: result.translation };\n } catch (err) {\n console.warn('[translateToEnglish] error:', err);\n return { translated: false, text };\n }\n}\n","import type { Plugin } from '@opencode-ai/plugin';\nimport { isEnglish } from './detector.ts';\nimport { extractCodeBlocks, restoreCodeBlocks } from './extractor.ts';\nimport { translateToEnglish } from './translator.ts';\nimport type { TranslatorConfig } from './types.ts';\n\nexport const InputTranslatorPlugin: Plugin = async ({ client }) => {\n const apiKey = process.env.TRANSLATOR_API_KEY;\n const baseUrl = process.env.TRANSLATOR_BASE_URL;\n const model = process.env.TRANSLATOR_MODEL ?? 'gpt-5-nano-2025-08-07';\n\n if (!apiKey || !baseUrl) {\n await client.app.log({\n body: {\n service: 'input-translator',\n level: 'warn',\n message:\n 'Missing TRANSLATOR_API_KEY or TRANSLATOR_BASE_URL. Input Translator Plugin is disabled.',\n },\n });\n return {};\n }\n\n const config: TranslatorConfig = { apiKey, baseUrl, model };\n\n return {\n 'experimental.chat.messages.transform': async (_input, output) => {\n for (const message of output.messages) {\n if (message.info.role !== 'user') continue;\n\n for (const part of message.parts) {\n if (part.type !== 'text') continue;\n if (part.text.trim().length === 0) continue;\n if (part.synthetic === true || part.ignored === true) continue;\n if (part.metadata?.__translated === true) continue;\n if (isEnglish(part.text)) continue;\n\n const { prose, blocks } = extractCodeBlocks(part.text);\n if (prose.trim().length === 0) continue;\n\n const result = await translateToEnglish(prose, config);\n if (result.translated) {\n part.text = restoreCodeBlocks(result.text, blocks);\n part.metadata = { ...part.metadata, __translated: true };\n }\n }\n }\n },\n };\n};\n\nexport default InputTranslatorPlugin;\n"],"mappings":"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"}
|