@beg4660/translator 0.1.0 → 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/README.md +82 -58
- package/dist/cli.js +201 -0
- package/dist/cli.js.map +1 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -1,58 +1,82 @@
|
|
|
1
|
-
# @nelon/translator
|
|
2
|
-
|
|
3
|
-
Gemini(클라우드) / Ollama(로컬) 중 원하는 Provider로 **번역 API 호출**을 할 수 있는 작은 TypeScript 라이브러리입니다.
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Node 18
|
|
12
|
-
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
### Gemini
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
import { GeminiProvider, TranslatorClient } from "@beg4660/translator";
|
|
19
|
-
|
|
20
|
-
const provider = new GeminiProvider({
|
|
21
|
-
apiKey: process.env.GEMINI_API_KEY!,
|
|
22
|
-
model: "gemini-1.5-flash"
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const client = new TranslatorClient(provider);
|
|
26
|
-
const result = await client.translate({
|
|
27
|
-
text: "Hello, how are you?",
|
|
28
|
-
targetLang: "Korean"
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
console.log(result.text);
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
### Ollama
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
1
|
+
# @nelon/translator
|
|
2
|
+
|
|
3
|
+
Gemini(클라우드) / Ollama(로컬) 중 원하는 Provider로 **번역 API 호출**을 할 수 있는 작은 TypeScript 라이브러리입니다.
|
|
4
|
+
|
|
5
|
+
## 설치
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @beg4660/translator
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Node.js 18 이상이 필요합니다.
|
|
12
|
+
|
|
13
|
+
## 사용법
|
|
14
|
+
|
|
15
|
+
### Gemini
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { GeminiProvider, TranslatorClient } from "@beg4660/translator";
|
|
19
|
+
|
|
20
|
+
const provider = new GeminiProvider({
|
|
21
|
+
apiKey: process.env.GEMINI_API_KEY!,
|
|
22
|
+
model: "gemini-1.5-flash" // 기본값, 생략 가능
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const client = new TranslatorClient(provider);
|
|
26
|
+
const result = await client.translate({
|
|
27
|
+
text: "Hello, how are you?",
|
|
28
|
+
targetLang: "Korean"
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
console.log(result.text); // 안녕하세요, 어떻게 지내세요?
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Ollama
|
|
35
|
+
|
|
36
|
+
Ollama가 로컬에서 실행 중이어야 합니다.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { OllamaProvider, TranslatorClient } from "@beg4660/translator";
|
|
40
|
+
|
|
41
|
+
const provider = new OllamaProvider({
|
|
42
|
+
host: "http://127.0.0.1:11434", // 기본값, 생략 가능
|
|
43
|
+
model: "llama3"
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const client = new TranslatorClient(provider);
|
|
47
|
+
const result = await client.translate({
|
|
48
|
+
text: "오늘 회의는 3시에 시작합니다.",
|
|
49
|
+
targetLang: "English",
|
|
50
|
+
tone: "formal"
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
console.log(result.text);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 번역 옵션
|
|
57
|
+
|
|
58
|
+
| 옵션 | 타입 | 필수 | 설명 |
|
|
59
|
+
|------|------|------|------|
|
|
60
|
+
| `text` | `string` | 필수 | 번역할 텍스트 |
|
|
61
|
+
| `targetLang` | `string` | 필수 | 번역 대상 언어 (예: `"Korean"`, `"English"`) |
|
|
62
|
+
| `sourceLang` | `string` | 선택 | 원본 언어 (기본값: 자동 감지) |
|
|
63
|
+
| `tone` | `TranslateTone` | 선택 | 번역 어조 (기본값: `"neutral"`) |
|
|
64
|
+
| `glossary` | `Record<string, string>` | 선택 | 고정 용어 사전 |
|
|
65
|
+
|
|
66
|
+
**tone 값:** `"neutral"` `"formal"` `"informal"` `"friendly"` `"technical"`
|
|
67
|
+
|
|
68
|
+
### 용어 사전 사용 예시
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const result = await client.translate({
|
|
72
|
+
text: "The API returned an error.",
|
|
73
|
+
targetLang: "Korean",
|
|
74
|
+
tone: "technical",
|
|
75
|
+
glossary: { "API": "API", "error": "오류" }
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Notes
|
|
80
|
+
|
|
81
|
+
- Gemini API Key는 앱/확장 쪽에서는 `vscode.SecretStorage` 같은 안전한 저장소를 권장합니다.
|
|
82
|
+
- Ollama는 기본적으로 `http://127.0.0.1:11434` 를 사용합니다.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as readline from 'readline/promises';
|
|
3
|
+
import { stdout, stdin } from 'process';
|
|
4
|
+
import { parseArgs } from 'util';
|
|
5
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
6
|
+
|
|
7
|
+
// src/prompt.ts
|
|
8
|
+
function formatGlossary(glossary) {
|
|
9
|
+
if (!glossary) return "";
|
|
10
|
+
const entries = Object.entries(glossary);
|
|
11
|
+
if (entries.length === 0) return "";
|
|
12
|
+
return entries.map(([k, v]) => `- "${k}" => "${v}"`).join("\n");
|
|
13
|
+
}
|
|
14
|
+
function buildTranslatePrompt(opts) {
|
|
15
|
+
const source = opts.sourceLang?.trim() ? opts.sourceLang.trim() : "auto";
|
|
16
|
+
const tone = opts.tone ?? "neutral";
|
|
17
|
+
const glossary = formatGlossary(opts.glossary);
|
|
18
|
+
return [
|
|
19
|
+
"You are a professional translation engine.",
|
|
20
|
+
"Translate the user's text precisely and naturally.",
|
|
21
|
+
"",
|
|
22
|
+
"Rules:",
|
|
23
|
+
"- Output ONLY the translated text. No quotes, no markdown, no explanations.",
|
|
24
|
+
"- Preserve line breaks.",
|
|
25
|
+
"- Keep code blocks, URLs, file paths, and identifiers unchanged.",
|
|
26
|
+
"- If the input is already in the target language, return it unchanged.",
|
|
27
|
+
glossary ? `- Use this glossary strictly (source => target):
|
|
28
|
+
${glossary}` : "- No glossary provided.",
|
|
29
|
+
"",
|
|
30
|
+
`Source language: ${source}`,
|
|
31
|
+
`Target language: ${opts.targetLang}`,
|
|
32
|
+
`Tone: ${tone}`,
|
|
33
|
+
"",
|
|
34
|
+
"Text to translate:",
|
|
35
|
+
opts.text
|
|
36
|
+
].join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/client.ts
|
|
40
|
+
var TranslatorClient = class {
|
|
41
|
+
constructor(provider) {
|
|
42
|
+
this.provider = provider;
|
|
43
|
+
}
|
|
44
|
+
provider;
|
|
45
|
+
async translate(opts) {
|
|
46
|
+
if (!opts?.text) throw new Error("translate: text is required");
|
|
47
|
+
if (!opts?.targetLang) throw new Error("translate: targetLang is required");
|
|
48
|
+
const prompt = buildTranslatePrompt(opts);
|
|
49
|
+
const out = await this.provider.generate(prompt);
|
|
50
|
+
return {
|
|
51
|
+
text: cleanupText(out.text),
|
|
52
|
+
provider: this.provider.kind,
|
|
53
|
+
model: this.provider.model,
|
|
54
|
+
raw: out.raw
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function cleanupText(s) {
|
|
59
|
+
return s.replace(/\r\n/g, "\n").replace(/[ \t]+\n/g, "\n").trimEnd();
|
|
60
|
+
}
|
|
61
|
+
var GeminiProvider = class {
|
|
62
|
+
kind = "gemini";
|
|
63
|
+
model;
|
|
64
|
+
client;
|
|
65
|
+
constructor(opts) {
|
|
66
|
+
if (!opts?.apiKey) throw new Error("GeminiProvider: apiKey is required");
|
|
67
|
+
this.model = opts.model ?? "gemini-1.5-flash";
|
|
68
|
+
this.client = new GoogleGenerativeAI(opts.apiKey);
|
|
69
|
+
}
|
|
70
|
+
async generate(prompt) {
|
|
71
|
+
const model = this.client.getGenerativeModel({ model: this.model });
|
|
72
|
+
const res = await model.generateContent(prompt);
|
|
73
|
+
const text = res.response.text();
|
|
74
|
+
return { text, raw: res };
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/providers/ollama.ts
|
|
79
|
+
var OllamaProvider = class {
|
|
80
|
+
kind = "ollama";
|
|
81
|
+
model;
|
|
82
|
+
host;
|
|
83
|
+
constructor(opts) {
|
|
84
|
+
if (!opts?.model) throw new Error("OllamaProvider: model is required");
|
|
85
|
+
this.model = opts.model;
|
|
86
|
+
this.host = (opts.host ?? "http://127.0.0.1:11434").replace(/\/+$/, "");
|
|
87
|
+
}
|
|
88
|
+
async generate(prompt) {
|
|
89
|
+
const body = {
|
|
90
|
+
model: this.model,
|
|
91
|
+
prompt,
|
|
92
|
+
stream: false
|
|
93
|
+
};
|
|
94
|
+
const res = await fetch(`${this.host}/api/generate`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: { "content-type": "application/json" },
|
|
97
|
+
body: JSON.stringify(body)
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
const msg = await safeReadText(res);
|
|
101
|
+
throw new Error(`OllamaProvider: HTTP ${res.status} ${res.statusText}${msg ? ` - ${msg}` : ""}`);
|
|
102
|
+
}
|
|
103
|
+
const json = await res.json();
|
|
104
|
+
return { text: (json.response ?? "").toString(), raw: json };
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
async function safeReadText(res) {
|
|
108
|
+
try {
|
|
109
|
+
return await res.text();
|
|
110
|
+
} catch {
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/cli.ts
|
|
116
|
+
var { values, positionals } = parseArgs({
|
|
117
|
+
allowPositionals: true,
|
|
118
|
+
options: {
|
|
119
|
+
to: { type: "string", short: "t" },
|
|
120
|
+
from: { type: "string", short: "f" },
|
|
121
|
+
tone: { type: "string" },
|
|
122
|
+
provider: { type: "string", short: "p", default: "gemini" },
|
|
123
|
+
model: { type: "string", short: "m" },
|
|
124
|
+
host: { type: "string" },
|
|
125
|
+
help: { type: "boolean", short: "h" }
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
if (values.help) {
|
|
129
|
+
console.log(`
|
|
130
|
+
Usage: translator [text] [options]
|
|
131
|
+
|
|
132
|
+
\uC778\uC218 \uC5C6\uC774 \uC2E4\uD589\uD558\uBA74 \uB300\uD654\uD615 \uBAA8\uB4DC\uB85C \uC9C4\uC785\uD569\uB2C8\uB2E4.
|
|
133
|
+
|
|
134
|
+
Options:
|
|
135
|
+
-t, --to <lang> \uBC88\uC5ED \uB300\uC0C1 \uC5B8\uC5B4 (\uAE30\uBCF8\uAC12: Korean)
|
|
136
|
+
-f, --from <lang> \uC6D0\uBCF8 \uC5B8\uC5B4 (\uAE30\uBCF8\uAC12: \uC790\uB3D9 \uAC10\uC9C0)
|
|
137
|
+
--tone <tone> \uC5B4\uC870: neutral | formal | informal | friendly | technical
|
|
138
|
+
-p, --provider <name> \uC0AC\uC6A9\uD560 provider: gemini | ollama (\uAE30\uBCF8\uAC12: gemini)
|
|
139
|
+
-m, --model <name> \uBAA8\uB378 \uC774\uB984
|
|
140
|
+
--host <url> Ollama \uD638\uC2A4\uD2B8 (\uAE30\uBCF8\uAC12: http://127.0.0.1:11434)
|
|
141
|
+
-h, --help \uB3C4\uC6C0\uB9D0
|
|
142
|
+
|
|
143
|
+
Examples:
|
|
144
|
+
translator # \uB300\uD654\uD615 \uBAA8\uB4DC
|
|
145
|
+
translator "Hello, world!" --to Korean
|
|
146
|
+
translator "\uC548\uB155\uD558\uC138\uC694" --to English --tone formal
|
|
147
|
+
translator "Hello" --to Korean --provider ollama --model llama3
|
|
148
|
+
GEMINI_API_KEY=xxx translator "Hello" --to Korean
|
|
149
|
+
`);
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
152
|
+
var targetLang = values.to ?? "Korean";
|
|
153
|
+
var providerKind = values.provider ?? "gemini";
|
|
154
|
+
var client;
|
|
155
|
+
if (providerKind === "ollama") {
|
|
156
|
+
const model = values.model ?? "llama3";
|
|
157
|
+
client = new TranslatorClient(
|
|
158
|
+
new OllamaProvider({ model, host: values.host })
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
162
|
+
if (!apiKey) {
|
|
163
|
+
console.error("\uC624\uB958: GEMINI_API_KEY \uD658\uACBD\uBCC0\uC218\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
client = new TranslatorClient(
|
|
167
|
+
new GeminiProvider({ apiKey, model: values.model })
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
async function translateText(text) {
|
|
171
|
+
const result = await client.translate({
|
|
172
|
+
text,
|
|
173
|
+
targetLang,
|
|
174
|
+
sourceLang: values.from,
|
|
175
|
+
tone: values.tone
|
|
176
|
+
});
|
|
177
|
+
console.log(result.text);
|
|
178
|
+
}
|
|
179
|
+
if (positionals.length > 0) {
|
|
180
|
+
await translateText(positionals.join(" "));
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
console.log(`\uBC88\uC5ED\uAE30 (${targetLang}) \u2014 \uC885\uB8CC\uD558\uB824\uBA74 Ctrl+C \uB610\uB294 /exit
|
|
184
|
+
`);
|
|
185
|
+
var rl = readline.createInterface({ input: stdin, output: stdout });
|
|
186
|
+
rl.on("close", () => {
|
|
187
|
+
console.log("\n\uC885\uB8CC\uD569\uB2C8\uB2E4.");
|
|
188
|
+
process.exit(0);
|
|
189
|
+
});
|
|
190
|
+
while (true) {
|
|
191
|
+
const text = await rl.question("> ").catch(() => null);
|
|
192
|
+
if (text === null || text.trim() === "/exit") {
|
|
193
|
+
rl.close();
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
if (text.trim() === "") continue;
|
|
197
|
+
await translateText(text);
|
|
198
|
+
console.log();
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=cli.js.map
|
|
201
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/prompt.ts","../src/client.ts","../src/providers/gemini.ts","../src/providers/ollama.ts","../src/cli.ts"],"names":["input","output"],"mappings":"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","file":"cli.js","sourcesContent":["import type { TranslateOptions } from \"./types.js\";\r\n\r\nfunction formatGlossary(glossary?: Record<string, string>): string {\r\n if (!glossary) return \"\";\r\n const entries = Object.entries(glossary);\r\n if (entries.length === 0) return \"\";\r\n return entries.map(([k, v]) => `- \"${k}\" => \"${v}\"`).join(\"\\n\");\r\n}\r\n\r\nexport function buildTranslatePrompt(opts: TranslateOptions): string {\r\n const source = opts.sourceLang?.trim() ? opts.sourceLang.trim() : \"auto\";\r\n const tone = opts.tone ?? \"neutral\";\r\n const glossary = formatGlossary(opts.glossary);\r\n\r\n return [\r\n \"You are a professional translation engine.\",\r\n \"Translate the user's text precisely and naturally.\",\r\n \"\",\r\n \"Rules:\",\r\n \"- Output ONLY the translated text. No quotes, no markdown, no explanations.\",\r\n \"- Preserve line breaks.\",\r\n \"- Keep code blocks, URLs, file paths, and identifiers unchanged.\",\r\n \"- If the input is already in the target language, return it unchanged.\",\r\n glossary\r\n ? `- Use this glossary strictly (source => target):\\n${glossary}`\r\n : \"- No glossary provided.\",\r\n \"\",\r\n `Source language: ${source}`,\r\n `Target language: ${opts.targetLang}`,\r\n `Tone: ${tone}`,\r\n \"\",\r\n \"Text to translate:\",\r\n opts.text\r\n ].join(\"\\n\");\r\n}\r\n\r\n","import { buildTranslatePrompt } from \"./prompt.js\";\r\nimport type { LLMProvider, TranslateOptions, TranslateResult } from \"./types.js\";\r\n\r\nexport class TranslatorClient {\r\n constructor(private readonly provider: LLMProvider) {}\r\n\r\n async translate(opts: TranslateOptions): Promise<TranslateResult> {\r\n if (!opts?.text) throw new Error(\"translate: text is required\");\r\n if (!opts?.targetLang) throw new Error(\"translate: targetLang is required\");\r\n\r\n const prompt = buildTranslatePrompt(opts);\r\n const out = await this.provider.generate(prompt);\r\n\r\n return {\r\n text: cleanupText(out.text),\r\n provider: this.provider.kind,\r\n model: this.provider.model,\r\n raw: out.raw\r\n };\r\n }\r\n}\r\n\r\nfunction cleanupText(s: string): string {\r\n // Keep it conservative: just normalize trailing whitespace/newlines.\r\n return s.replace(/\\r\\n/g, \"\\n\").replace(/[ \\t]+\\n/g, \"\\n\").trimEnd();\r\n}\r\n\r\n","import { GoogleGenerativeAI } from \"@google/generative-ai\";\r\nimport type { GeminiProviderOptions, LLMProvider } from \"../types.js\";\r\n\r\nexport class GeminiProvider implements LLMProvider {\r\n readonly kind = \"gemini\" as const;\r\n readonly model: string;\r\n private readonly client: GoogleGenerativeAI;\r\n\r\n constructor(opts: GeminiProviderOptions) {\r\n if (!opts?.apiKey) throw new Error(\"GeminiProvider: apiKey is required\");\r\n this.model = opts.model ?? \"gemini-1.5-flash\";\r\n this.client = new GoogleGenerativeAI(opts.apiKey);\r\n }\r\n\r\n async generate(prompt: string): Promise<{ text: string; raw?: unknown }> {\r\n const model = this.client.getGenerativeModel({ model: this.model });\r\n const res = await model.generateContent(prompt);\r\n const text = res.response.text();\r\n return { text, raw: res };\r\n }\r\n}\r\n\r\n","import type { LLMProvider, OllamaProviderOptions } from \"../types.js\";\r\n\r\ntype OllamaGenerateRequest = {\r\n model: string;\r\n prompt: string;\r\n stream?: boolean;\r\n options?: Record<string, unknown>;\r\n};\r\n\r\ntype OllamaGenerateResponse = {\r\n response?: string;\r\n done?: boolean;\r\n model?: string;\r\n};\r\n\r\nexport class OllamaProvider implements LLMProvider {\r\n readonly kind = \"ollama\" as const;\r\n readonly model: string;\r\n private readonly host: string;\r\n\r\n constructor(opts: OllamaProviderOptions) {\r\n if (!opts?.model) throw new Error(\"OllamaProvider: model is required\");\r\n this.model = opts.model;\r\n this.host = (opts.host ?? \"http://127.0.0.1:11434\").replace(/\\/+$/, \"\");\r\n }\r\n\r\n async generate(prompt: string): Promise<{ text: string; raw?: unknown }> {\r\n const body: OllamaGenerateRequest = {\r\n model: this.model,\r\n prompt,\r\n stream: false\r\n };\r\n\r\n const res = await fetch(`${this.host}/api/generate`, {\r\n method: \"POST\",\r\n headers: { \"content-type\": \"application/json\" },\r\n body: JSON.stringify(body)\r\n });\r\n\r\n if (!res.ok) {\r\n const msg = await safeReadText(res);\r\n throw new Error(`OllamaProvider: HTTP ${res.status} ${res.statusText}${msg ? ` - ${msg}` : \"\"}`);\r\n }\r\n\r\n const json = (await res.json()) as OllamaGenerateResponse;\r\n return { text: (json.response ?? \"\").toString(), raw: json };\r\n }\r\n}\r\n\r\nasync function safeReadText(res: Response): Promise<string> {\r\n try {\r\n return await res.text();\r\n } catch {\r\n return \"\";\r\n }\r\n}\r\n\r\n","import * as readline from \"node:readline/promises\";\nimport { stdin as input, stdout as output } from \"node:process\";\nimport { parseArgs } from \"node:util\";\nimport { TranslatorClient } from \"./client.js\";\nimport { GeminiProvider } from \"./providers/gemini.js\";\nimport { OllamaProvider } from \"./providers/ollama.js\";\nimport type { TranslateTone } from \"./types.js\";\n\nconst { values, positionals } = parseArgs({\n allowPositionals: true,\n options: {\n to: { type: \"string\", short: \"t\" },\n from: { type: \"string\", short: \"f\" },\n tone: { type: \"string\" },\n provider: { type: \"string\", short: \"p\", default: \"gemini\" },\n model: { type: \"string\", short: \"m\" },\n host: { type: \"string\" },\n help: { type: \"boolean\", short: \"h\" },\n },\n});\n\nif (values.help) {\n console.log(`\nUsage: translator [text] [options]\n\n인수 없이 실행하면 대화형 모드로 진입합니다.\n\nOptions:\n -t, --to <lang> 번역 대상 언어 (기본값: Korean)\n -f, --from <lang> 원본 언어 (기본값: 자동 감지)\n --tone <tone> 어조: neutral | formal | informal | friendly | technical\n -p, --provider <name> 사용할 provider: gemini | ollama (기본값: gemini)\n -m, --model <name> 모델 이름\n --host <url> Ollama 호스트 (기본값: http://127.0.0.1:11434)\n -h, --help 도움말\n\nExamples:\n translator # 대화형 모드\n translator \"Hello, world!\" --to Korean\n translator \"안녕하세요\" --to English --tone formal\n translator \"Hello\" --to Korean --provider ollama --model llama3\n GEMINI_API_KEY=xxx translator \"Hello\" --to Korean\n`);\n process.exit(0);\n}\n\nconst targetLang = values.to ?? \"Korean\";\nconst providerKind = values.provider ?? \"gemini\";\n\nlet client: TranslatorClient;\n\nif (providerKind === \"ollama\") {\n const model = values.model ?? \"llama3\";\n client = new TranslatorClient(\n new OllamaProvider({ model, host: values.host })\n );\n} else {\n const apiKey = process.env.GEMINI_API_KEY;\n if (!apiKey) {\n console.error(\"오류: GEMINI_API_KEY 환경변수가 필요합니다.\");\n process.exit(1);\n }\n client = new TranslatorClient(\n new GeminiProvider({ apiKey, model: values.model })\n );\n}\n\nasync function translateText(text: string): Promise<void> {\n const result = await client.translate({\n text,\n targetLang,\n sourceLang: values.from,\n tone: values.tone as TranslateTone | undefined,\n });\n console.log(result.text);\n}\n\n// 인수가 있으면 단일 번역 후 종료\nif (positionals.length > 0) {\n await translateText(positionals.join(\" \"));\n process.exit(0);\n}\n\n// 대화형 모드\nconsole.log(`번역기 (${targetLang}) — 종료하려면 Ctrl+C 또는 /exit\\n`);\n\nconst rl = readline.createInterface({ input, output });\n\nrl.on(\"close\", () => {\n console.log(\"\\n종료합니다.\");\n process.exit(0);\n});\n\nwhile (true) {\n const text = await rl.question(\"> \").catch(() => null);\n\n if (text === null || text.trim() === \"/exit\") {\n rl.close();\n break;\n }\n\n if (text.trim() === \"\") continue;\n\n await translateText(text);\n console.log();\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beg4660/translator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Unified Gemini/Ollama translation client",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
"require": "./dist/index.cjs"
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"translator": "./dist/cli.js"
|
|
23
|
+
},
|
|
21
24
|
"files": [
|
|
22
25
|
"dist"
|
|
23
26
|
],
|
|
@@ -33,9 +36,11 @@
|
|
|
33
36
|
"prepublishOnly": "npm run build"
|
|
34
37
|
},
|
|
35
38
|
"dependencies": {
|
|
39
|
+
"@beg4660/translator": "^0.1.0",
|
|
36
40
|
"@google/generative-ai": "^0.24.1"
|
|
37
41
|
},
|
|
38
42
|
"devDependencies": {
|
|
43
|
+
"@types/node": "^25.5.0",
|
|
39
44
|
"tsup": "^8.5.0",
|
|
40
45
|
"typescript": "^5.8.3"
|
|
41
46
|
}
|