opencode-input-translator 0.1.3 → 0.2.0
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 +132 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,2 +1,133 @@
|
|
|
1
|
-
import{createOpenAICompatible
|
|
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
|
+
providerOptions: { openai: { textVerbosity: "low" } }
|
|
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 };
|
|
2
133
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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 {
|
|
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 {\n createOpenAICompatible,\n type OpenAICompatibleLanguageModelChatOptions,\n} 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 providerOptions: {\n openai: {\n textVerbosity: 'low',\n } satisfies OpenAICompatibleLanguageModelChatOptions,\n },\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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"}
|