gpt-po 1.2.3 → 1.3.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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm install *)"
5
+ ]
6
+ }
7
+ }
package/README.md CHANGED
@@ -5,6 +5,8 @@
5
5
 
6
6
  Translation tool for gettext (po) files that supports custom system prompts and user dictionaries. It also supports translating specified po files to a designated target language and updating po files based on pot files.
7
7
 
8
+ **Supported LLM Providers:** OpenAI (GPT), Anthropic (Claude), and Google (Gemini)
9
+
8
10
  Read in other languages: English | [简体中文](./README_zh-CN.md)
9
11
 
10
12
  <a href="https://buymeacoffee.com/ryanhex" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-red.png" alt="Buy Me A Coffee" height="41" width="174"></a>
@@ -15,9 +17,18 @@ Read in other languages: English | [简体中文](./README_zh-CN.md)
15
17
  npm install gpt-po
16
18
  ```
17
19
 
18
- Set `OPENAI_API_KEY` before using this tool.
20
+ Set `API_KEY` before using this tool. Depending on the provider, use `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or `GEMINI_API_KEY`.
21
+
22
+ **It is recommended to use paid APIs to improve translation speed, as free tiers are often slower and have usage restrictions.**
19
23
 
20
- **It is recommended to use the paid OpenAI API to improve translation speed, as the free OpenAI API is slower (only 3 translations per minute) and has usage restrictions.**
24
+ ### Environment Variables
25
+ - `OPENAI_API_KEY`: OpenAI API key.
26
+ - `OPENAI_API_HOST`: OpenAI API host (default: `https://api.openai.com`).
27
+ - `ANTHROPIC_API_KEY`: Anthropic API key.
28
+ - `ANTHROPIC_API_HOST`: Anthropic API host.
29
+ - `GEMINI_API_KEY`: Google Gemini API key.
30
+ - `GEMINI_API_HOST`: Google Gemini API host.
31
+ - `MODEL_TMP`: Model temperature (default: 0.1). This will override provider-specific temperature settings.
21
32
 
22
33
  ## Usage Scenarios
23
34
 
@@ -51,9 +62,10 @@ Usage: gpt-po [options]
51
62
  translate po file (default command)
52
63
 
53
64
  Options:
54
- -k, --key <key> openai api key (env: OPENAI_API_KEY)
55
- --host <host> openai api host (env: OPENAI_API_HOST)
56
- --model <model> openai model (default: "gpt-4o-mini", env: OPENAI_MODEL)
65
+ -p, --provider <provider> API provider (choices: "openai", "anthropic", "gemini", default: "openai")
66
+ -k, --key <key> API key (can also be set via OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY)
67
+ --host <host> API host (can also be set via OPENAI_API_HOST, ANTHROPIC_API_HOST, GEMINI_API_HOST)
68
+ --model <model> Model to use (default: gpt-5-nano for openai, claude-haiku-4-5 for anthropic, gemini-2.5-flash for gemini, env: MODEL)
57
69
  --po <file> po file path
58
70
  --dir <dir> po file directory
59
71
  -src, --source <lang> source language (default: "english")
@@ -61,6 +73,8 @@ Options:
61
73
  -l, --lang <lang> target language (ISO 639-1 code)
62
74
  -o, --output <file> output file path, overwirte po file by default
63
75
  --context <file> context file path (provides additional context to the bot)
76
+ --context-length <length> Maximum accumulated length of source strings (msgid) to translate in each API request (default: 2000, env: API_CONTEXT_LENGTH)
77
+ --timeout <ms> Timeout in milliseconds for API requests (default: 20000, env: API_TIMEOUT)
64
78
  -h, --help display help for command
65
79
  ```
66
80
 
@@ -90,4 +104,4 @@ Options:
90
104
  --po <file> po file path
91
105
  --pot <file> pot file path
92
106
  -h, --help display help for command
93
- ```
107
+ ```
package/README_zh-CN.md CHANGED
@@ -5,6 +5,8 @@
5
5
 
6
6
  gettext(po)文件翻译工具,支持自定义系统提示词和用户字典,支持翻译指定的po文件到指定的目标语言,支持根据pot文件更新po文件。
7
7
 
8
+ **支持的LLM提供商:** OpenAI (GPT)、Anthropic (Claude) 和 Google (Gemini)
9
+
8
10
  使用其他语言阅读:[English](./README.md) | 简体中文
9
11
 
10
12
  <a href="https://buymeacoffee.com/ryanhex" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-red.png" alt="请我喝杯咖啡" height="41" width="174"></a>
@@ -15,11 +17,20 @@ gettext(po)文件翻译工具,支持自定义系统提示词和用户字典,
15
17
  npm install gpt-po
16
18
  ```
17
19
 
18
- 使用此工具前先设置 `OPENAI_API_KEY`,Windows中使用 `set OPENAI_API_KEY=<key>`, Linux中 `export OPENAI_API_KEY=<key>`
20
+ 使用此工具前先设置 `API_KEY`,根据不同的提供商,使用 `OPENAI_API_KEY`,`ANTHROPIC_API_KEY` `GEMINI_API_KEY`。
21
+
22
+ **建议使用付费API以提高翻译速度,免费API速度较慢且有使用限制。**
19
23
 
20
- **建议使用付费的OpenAI API以提高翻译速度,免费的OpenAI API速度较慢(一分钟3条一天200条),且有使用限制。**
24
+ *国内用户要设置`HTTPS_PROXY`环境变量上代理才能用,或者配置使用代理服务的HOST*
21
25
 
22
- *国内用户要设置`HTTPS_PROXY`环境变量上梯子才能用*
26
+ ### 环境变量
27
+ - `OPENAI_API_KEY`: OpenAI API密钥。
28
+ - `OPENAI_API_HOST`: OpenAI API主机(默认:`https://api.openai.com`)。
29
+ - `ANTHROPIC_API_KEY`: Anthropic API密钥。
30
+ - `ANTHROPIC_API_HOST`: Anthropic API主机。
31
+ - `GEMINI_API_KEY`: Google Gemini API密钥。
32
+ - `GEMINI_API_HOST`: Google Gemini API主机。
33
+ - `MODEL_TMP`: 模型温度(默认:0.1)。这将覆盖特定于提供商的温度设置。
23
34
 
24
35
  ## 常见用法
25
36
 
@@ -53,9 +64,10 @@ npm install gpt-po
53
64
  翻译 po 文件(默认命令)
54
65
 
55
66
  选项:
56
- -k, --key <key> openai api key (环境变量: OPENAI_API_KEY)
57
- --host <host> openai api host (环境变量: OPENAI_API_HOST)
58
- --model <model> openai 模型 (默认: "gpt-4o-mini", 环境变量: OPENAI_MODEL)
67
+ -p, --provider <provider> API 提供商 (可选: "openai", "anthropic", "gemini", 默认: "openai")
68
+ -k, --key <key> API 密钥 (也可通过 OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY 设置)
69
+ --host <host> API 主机 (也可通过 OPENAI_API_HOST, ANTHROPIC_API_HOST, GEMINI_API_HOST 设置)
70
+ --model <model> 要使用的模型 (默认: openai 为 gpt-5-nano, anthropic 为 claude-haiku-4-5, gemini 为 gemini-2.5-flash, 环境变量: MODEL)
59
71
  --po <file> po 文件路径
60
72
  --dir <dir> po 文件目录
61
73
  -src, --source <lang> 源语言 (默认: "english")
@@ -63,6 +75,8 @@ npm install gpt-po
63
75
  -l, --lang <lang> 目标语言 (ISO 639-1 代码)
64
76
  -o, --output <file> 输出文件路径,默认覆盖 po 文件
65
77
  --context <file> 上下文文件路径(为机器人提供额外的上下文)
78
+ --context-length <length> 每次 API 请求中源字符串(msgid)的最大累计长度(默认:2000,环境变量:API_CONTEXT_LENGTH)
79
+ --timeout <ms> API 请求超时时间(单位:毫秒,默认:20000,环境变量:API_TIMEOUT)
66
80
  -h, --help 显示命令帮助
67
81
  ```
68
82
 
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gpt-po",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "command tool for translate po files by gpt",
5
5
  "main": "lib/src/index.js",
6
6
  "type": "module",
@@ -11,7 +11,7 @@
11
11
  "start": "node lib/src/index.js",
12
12
  "test": "echo \"Error: no test specified\" && exit 1",
13
13
  "prebuild": "rimraf ./lib",
14
- "build": "tsc",
14
+ "build": "tsc -p tsconfig.json",
15
15
  "postbuild": "ncp ./src/ ./lib/src/ \"--filter=^(?!.*\\.ts$).*$\"",
16
16
  "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
17
17
  "lint": "tslint -p tsconfig.json"
@@ -34,7 +34,7 @@
34
34
  "openai"
35
35
  ],
36
36
  "devDependencies": {
37
- "@types/gettext-parser": "^4.0.4",
37
+ "@types/gettext-parser": "^9.0.0",
38
38
  "@types/jest": "^29.5.12",
39
39
  "jest": "^29.7.0",
40
40
  "ncp": "^2.0.0",
@@ -46,11 +46,13 @@
46
46
  "typescript": "^5.5.4"
47
47
  },
48
48
  "dependencies": {
49
+ "@anthropic-ai/sdk": "^0.99.0",
50
+ "@google/genai": "^2.6.0",
49
51
  "commander": "^12.1.0",
50
- "gettext-parser": "^8.0.0",
51
- "openai": "^4.56.0"
52
+ "gettext-parser": "^9.0.1",
53
+ "openai": "^6.39.0"
52
54
  },
53
55
  "engines": {
54
- "node": ">=18.0.0"
56
+ "node": ">=20.0.0"
55
57
  }
56
58
  }
package/lib/src/index.js CHANGED
@@ -30,37 +30,58 @@ class SharedOptionsCommand extends Command {
30
30
  }
31
31
  const translateCommand = new SharedOptionsCommand("translate")
32
32
  .description("translate po file (default command)")
33
- .addOption(new Option("-k, --key <key>", "openai api key").env("OPENAI_API_KEY"))
34
- .addOption(new Option("--host <host>", "openai api host").env("OPENAI_API_HOST"))
35
- .addOption(new Option("--model <model>", "openai model").env("OPENAI_MODEL").default("gpt-4o-mini"))
33
+ .addOption(new Option("-p, --provider <provider>", "api provider").choices(["openai", "anthropic", "gemini"]).default("openai"))
34
+ .addOption(new Option("-k, --key <key>", "api key (can also be set via OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY)"))
35
+ .addOption(new Option("--host <host>", "api host (can also be set via OPENAI_API_HOST, ANTHROPIC_API_HOST, GEMINI_API_HOST)"))
36
+ .addOption(new Option("--model <model>", "model to use (default: gpt-5-nano for openai, claude-haiku-4-5 for anthropic, gemini-2.5-flash for gemini)"))
36
37
  .addOption(new Option("--po <file>", "po file path").conflicts("dir"))
37
38
  .addOption(new Option("--dir <dir>", "po file directory").conflicts("po"))
38
39
  .option("-src, --source <lang>", "source language (ISO 639-1)", "en")
39
40
  .option("-l, --lang <lang>", "target language (ISO 639-1)")
40
41
  .option("--verbose", "print verbose log")
41
42
  .option("--context <file>", "text file that provides the bot additional context")
43
+ .addOption(new Option("--context-length <length>", "maximum accumulated length of source strings (msgid) to translate in each API request").env("API_CONTEXT_LENGTH").default("2000"))
44
+ .addOption(new Option("--timeout <ms>", "timeout in milliseconds for API requests").env("API_TIMEOUT").default("20000"))
42
45
  .addOption(new Option("-o, --output <file>", "output file path, overwirte po file by default").conflicts("dir"))
43
46
  .addCompileOptions()
44
47
  .action(async (args) => {
45
- const { key, host, model, po, dir, source, lang, verbose, output, context } = args;
48
+ const { provider, key, host, model, po, dir, source, lang, verbose, output, context, contextLength, timeout } = args;
46
49
  if (host) {
47
- process.env.OPENAI_API_HOST = host;
50
+ if (provider === "openai")
51
+ process.env.OPENAI_API_HOST = host;
52
+ if (provider === "anthropic")
53
+ process.env.ANTHROPIC_API_HOST = host;
54
+ if (provider === "gemini")
55
+ process.env.GEMINI_API_HOST = host;
48
56
  }
49
57
  if (key) {
50
- process.env.OPENAI_API_KEY = key;
58
+ if (provider === "openai")
59
+ process.env.OPENAI_API_KEY = key;
60
+ if (provider === "anthropic")
61
+ process.env.ANTHROPIC_API_KEY = key;
62
+ if (provider === "gemini")
63
+ process.env.GEMINI_API_KEY = key;
51
64
  }
52
- // process.env.OPENAI_API_KEY is not set, exit
53
- if (!process.env.OPENAI_API_KEY) {
65
+ if (provider === "openai" && !process.env.OPENAI_API_KEY) {
54
66
  console.error("OPENAI_API_KEY is required");
55
67
  process.exit(1);
56
68
  }
57
- init();
69
+ if (provider === "anthropic" && !process.env.ANTHROPIC_API_KEY) {
70
+ console.error("ANTHROPIC_API_KEY is required");
71
+ process.exit(1);
72
+ }
73
+ if (provider === "gemini" && !process.env.GEMINI_API_KEY) {
74
+ console.error("GEMINI_API_KEY is required");
75
+ process.exit(1);
76
+ }
77
+ const finalModel = model || process.env.MODEL || process.env.OPENAI_MODEL || (provider === "anthropic" ? "claude-haiku-4-5" : provider === "gemini" ? "gemini-2.5-flash" : "gpt-5-nano");
78
+ init(provider);
58
79
  const compileOptions = getCompileOptions(args);
59
80
  if (po) {
60
- await translatePo(model, po, source, lang, verbose, output, context, compileOptions);
81
+ await translatePo(provider, finalModel, po, source, lang, verbose, output, context, parseInt(contextLength), parseInt(timeout), compileOptions);
61
82
  }
62
83
  else if (dir) {
63
- await translatePoDir(model, dir, source, lang, verbose, context, compileOptions);
84
+ await translatePoDir(provider, finalModel, dir, source, lang, verbose, context, parseInt(contextLength), parseInt(timeout), compileOptions);
64
85
  }
65
86
  else {
66
87
  console.error("po file or directory is required");
@@ -15,10 +15,6 @@ export function removeByOptions(potrans, options) {
15
15
  if (options?.fuzzy && fuzzyRegx.test(entry.comments?.flag || "")) {
16
16
  delete potrans.translations[ctx][msgid];
17
17
  }
18
- // remove obsolete
19
- if (options?.obsolete && obsoleteRegx.test(entry.comments?.flag || "")) {
20
- delete potrans.translations[ctx][msgid];
21
- }
22
18
  // remove untranslated
23
19
  if (options?.untranslated && msgstr.length === 0) {
24
20
  delete potrans.translations[ctx][msgid];
@@ -48,6 +44,8 @@ export function removeByOptions(potrans, options) {
48
44
  }
49
45
  }
50
46
  }
47
+ if (options?.obsolete && potrans.obsolete)
48
+ delete potrans.obsolete;
51
49
  return potrans;
52
50
  }
53
51
  //# sourceMappingURL=manipulate.js.map
package/lib/src/sync.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { CompileOptions } from "./utils.js";
2
- export declare function sync(po: string, pot: string, compileOptions?: CompileOptions): Promise<void>;
1
+ import { GetTextPoCompilerOptions } from "gettext-parser";
2
+ export declare function sync(po: string, pot: string, compileOptions?: GetTextPoCompilerOptions): Promise<void>;
@@ -1,7 +1,5 @@
1
- import { GetTextTranslation } from "gettext-parser";
2
- import { OpenAI } from "openai";
3
- import { CompileOptions } from "./utils.js";
4
- export declare function init(force?: boolean): OpenAI;
5
- export declare function translate(src: string, lang: string, model: string, translations: GetTextTranslation[], contextFile: string): Promise<void>;
6
- export declare function translatePo(model: string, po: string, source: string, lang: string, verbose: boolean, output: string, contextFile: string, compileOptions?: CompileOptions): Promise<void>;
7
- export declare function translatePoDir(model: string | undefined, dir: string, source: string, lang: string, verbose: boolean, contextFile: string, compileOptions?: CompileOptions): Promise<void>;
1
+ import { GetTextPoCompilerOptions, GetTextTranslation } from "gettext-parser";
2
+ export declare function init(provider: string, force?: boolean): void;
3
+ export declare function translate(provider: string, src: string, lang: string, model: string, translations: GetTextTranslation[], contextFile: string, timeout: number): Promise<void>;
4
+ export declare function translatePo(provider: string, model: string, po: string, source: string, lang: string, verbose: boolean, output: string, contextFile: string, contextLength: number, timeout: number, compileOptions?: GetTextPoCompilerOptions): Promise<void>;
5
+ export declare function translatePoDir(provider: string, model: string, dir: string, source: string, lang: string, verbose: boolean, contextFile: string, contextLength: number, timeout: number, compileOptions?: GetTextPoCompilerOptions): Promise<void>;
@@ -1,5 +1,7 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import { GoogleGenAI } from "@google/genai";
1
3
  import * as fs from "fs";
2
- import { OpenAI } from "openai";
4
+ import OpenAI from "openai";
3
5
  import path from "path";
4
6
  import { fileURLToPath } from "url";
5
7
  import pkg from "../package.json" with { type: "json" };
@@ -7,18 +9,36 @@ import { compilePo, copyFileIfNotExists, findConfig, parsePo, printProgress } fr
7
9
  const __filename = fileURLToPath(import.meta.url);
8
10
  const __dirname = path.dirname(__filename);
9
11
  let _openai;
12
+ let _anthropic;
13
+ let _gemini;
10
14
  let _systemprompt;
11
15
  let _userprompt;
12
16
  let _userdict;
13
- export function init(force) {
14
- if (!_openai || force) {
15
- let configuration = {
16
- apiKey: process.env.OPENAI_API_KEY
17
+ export function init(provider, force) {
18
+ if (provider === "openai" && (!_openai || force)) {
19
+ _openai = new OpenAI({
20
+ apiKey: process.env.OPENAI_API_KEY,
21
+ baseURL: process.env.OPENAI_API_HOST
22
+ ? process.env.OPENAI_API_HOST.replace(/\/+$/, "") + "/v1"
23
+ : undefined
24
+ });
25
+ }
26
+ else if (provider === "anthropic" && (!_anthropic || force)) {
27
+ _anthropic = new Anthropic({
28
+ apiKey: process.env.ANTHROPIC_API_KEY,
29
+ baseURL: process.env.ANTHROPIC_API_HOST ? process.env.ANTHROPIC_API_HOST.replace(/\/+$/, "") : undefined
30
+ });
31
+ }
32
+ else if (provider === "gemini" && (!_gemini || force)) {
33
+ const options = {
34
+ apiKey: process.env.GEMINI_API_KEY || ""
17
35
  };
18
- _openai = new OpenAI(configuration);
19
- if (process.env.OPENAI_API_HOST) {
20
- _openai.baseURL = process.env.OPENAI_API_HOST.replace(/\/+$/, "") + "/v1";
36
+ if (process.env.GEMINI_API_HOST) {
37
+ options.httpOptions = {
38
+ baseUrl: process.env.GEMINI_API_HOST.replace(/\/+$/, "")
39
+ };
21
40
  }
41
+ _gemini = new GoogleGenAI(options);
22
42
  }
23
43
  // load systemprompt.txt from project
24
44
  if (!_systemprompt || force) {
@@ -34,9 +54,8 @@ export function init(force) {
34
54
  copyFileIfNotExists(userdict, path.join(__dirname, "dictionary.json"));
35
55
  _userdict = { default: JSON.parse(fs.readFileSync(userdict, "utf-8")) };
36
56
  }
37
- return _openai;
38
57
  }
39
- export async function translate(src, lang, model, translations, contextFile) {
58
+ export async function translate(provider, src, lang, model, translations, contextFile, timeout) {
40
59
  const lang_code = lang
41
60
  .toLowerCase()
42
61
  .trim()
@@ -48,56 +67,98 @@ export async function translate(src, lang, model, translations, contextFile) {
48
67
  }
49
68
  return acc;
50
69
  }, { user: [], assistant: [] });
51
- const notes = translations
52
- .reduce((acc, tr) => {
53
- if (tr.comments?.extracted) {
54
- acc.push(tr.comments?.extracted);
55
- }
56
- return acc;
57
- }, [])
58
- .join("\n");
70
+ const escapePseudoXmlAttr = (value) => String(value)
71
+ .replace(/&/g, "&amp;")
72
+ .replace(/"/g, "&quot;")
73
+ .replace(/</g, "&lt;")
74
+ .replace(/>/g, "&gt;")
75
+ .replace(/\r?\n/g, " &#10; ");
59
76
  const context = contextFile ? "\n\nContext: " + fs.readFileSync(contextFile, "utf-8") : "";
60
77
  const translationsContent = translations
61
78
  .map((tr, idx) => {
62
- const contextAttr = tr.msgctxt ? ` context="${tr.msgctxt}"` : "";
63
- return `<translate index="${idx + dicts.user.length + 1}"${contextAttr}>${tr.msgid}</translate>`;
79
+ const contextAttr = tr.msgctxt ? ` context="${escapePseudoXmlAttr(tr.msgctxt)}"` : "";
80
+ const noteAttr = tr.comments?.extracted ? ` note="${escapePseudoXmlAttr(tr.comments.extracted)}"` : "";
81
+ return `<translate index="${idx + dicts.user.length + 1}"${contextAttr}${noteAttr}>${tr.msgid}</translate>`;
64
82
  })
65
83
  .join("\n");
66
- const res = await _openai.chat.completions.create({
67
- model: model,
68
- temperature: 0.1,
69
- messages: [
70
- {
71
- role: "system",
72
- content: _systemprompt + context
73
- },
74
- {
75
- role: "user",
76
- content: `${_userprompt}\n\nWait for my incoming message in "${src}" and translate it into "${lang}"(a language code and an optional region code). ` +
77
- notes
78
- },
79
- {
80
- role: "assistant",
81
- content: `Understood, I will translate your incoming "${src}" message into "${lang}", carefully following guidelines. Please go ahead and send your message for translation.`
82
- },
83
- // add userdict
84
+ const temperature = process.env.MODEL_TMP
85
+ ? parseFloat(process.env.MODEL_TMP)
86
+ : process.env.OPENAI_MODEL_TMP
87
+ ? parseFloat(process.env.OPENAI_MODEL_TMP)
88
+ : 0.1;
89
+ const systemContent = _systemprompt + context;
90
+ const initialUserContent = `${_userprompt}\n\nWait for my incoming message(s) in \`${src}\` and translate them into \`${lang}\` (\`${src}\` and \`${lang}\` are XPG/POSIX locale names, used in Unix-like systems and GNU Gettext).`;
91
+ const initialAssistantContent = `Understood, I will translate your incoming \`${src}\` message(s) into \`${lang}\`, carefully following guidelines. Please go ahead and send your message(s) for translation.`;
92
+ let content = "";
93
+ if (provider === "openai") {
94
+ const messages = [
95
+ { role: "system", content: systemContent },
96
+ { role: "user", content: initialUserContent },
97
+ { role: "assistant", content: initialAssistantContent },
84
98
  ...(dicts.user.length > 0
85
99
  ? [
86
100
  { role: "user", content: dicts.user.join("\n") },
87
101
  { role: "assistant", content: dicts.assistant.join("\n") }
88
102
  ]
89
103
  : []),
90
- // add user translations
91
- {
92
- role: "user",
93
- content: translationsContent
104
+ { role: "user", content: translationsContent }
105
+ ];
106
+ const res = await _openai.chat.completions.create({ model, temperature, messages }, { timeout, stream: false });
107
+ content = res.choices[0].message.content ?? "";
108
+ }
109
+ else if (provider === "anthropic") {
110
+ const messages = [
111
+ { role: "user", content: initialUserContent },
112
+ { role: "assistant", content: initialAssistantContent },
113
+ ...(dicts.user.length > 0
114
+ ? [
115
+ { role: "user", content: dicts.user.join("\n") },
116
+ { role: "assistant", content: dicts.assistant.join("\n") }
117
+ ]
118
+ : []),
119
+ { role: "user", content: translationsContent }
120
+ ];
121
+ let res = await _anthropic.messages.create({ model, temperature, system: systemContent, messages, max_tokens: 4096 }, { timeout });
122
+ if (typeof res === "string") {
123
+ res = JSON.parse(res);
124
+ }
125
+ if (res.content && res.content.length > 0 && res.content[0].type === "text") {
126
+ content = res.content[0].text;
127
+ }
128
+ else {
129
+ console.error("Error: Anthropic response content is empty or not a text block.", res.content);
130
+ content = ""; // Default to empty string or handle error appropriately
131
+ }
132
+ }
133
+ else if (provider === "gemini") {
134
+ const contents = [
135
+ { role: "user", parts: [{ text: initialUserContent }] },
136
+ { role: "model", parts: [{ text: initialAssistantContent }] },
137
+ ...(dicts.user.length > 0
138
+ ? [
139
+ { role: "user", parts: [{ text: dicts.user.join("\n") }] },
140
+ { role: "model", parts: [{ text: dicts.assistant.join("\n") }] }
141
+ ]
142
+ : []),
143
+ { role: "user", parts: [{ text: translationsContent }] }
144
+ ];
145
+ const res = await _gemini.models.generateContent({
146
+ model,
147
+ contents,
148
+ config: {
149
+ systemInstruction: systemContent,
150
+ temperature,
151
+ maxOutputTokens: 4096,
152
+ httpOptions: { timeout }
94
153
  }
95
- ]
96
- }, {
97
- timeout: 20000,
98
- stream: false
99
- });
100
- const content = res.choices[0].message.content ?? "";
154
+ });
155
+ if (res.candidates && res.candidates.length > 0) {
156
+ const candidate = res.candidates[0];
157
+ if (candidate.content && candidate.content.parts && candidate.content.parts.length > 0) {
158
+ content = candidate.content.parts[0].text || "";
159
+ }
160
+ }
161
+ }
101
162
  translations.forEach((trans, idx) => {
102
163
  const tag = `<translated index="${idx + dicts.user.length + 1}">`;
103
164
  const s = content.indexOf(tag);
@@ -110,7 +171,7 @@ export async function translate(src, lang, model, translations, contextFile) {
110
171
  }
111
172
  });
112
173
  }
113
- export async function translatePo(model, po, source, lang, verbose, output, contextFile, compileOptions) {
174
+ export async function translatePo(provider, model, po, source, lang, verbose, output, contextFile, contextLength, timeout, compileOptions) {
114
175
  const potrans = await parsePo(po);
115
176
  if (!lang)
116
177
  lang = potrans.headers["Language"];
@@ -170,13 +231,13 @@ export async function translatePo(model, po, source, lang, verbose, output, cont
170
231
  await new Promise((resolve) => setTimeout(resolve, 20000));
171
232
  }
172
233
  const trans = list[i];
173
- if (c < 2000) {
234
+ if (c < contextLength) {
174
235
  translations.push(trans);
175
236
  c += trans.msgid.length;
176
237
  }
177
- if (c >= 2000 || i == list.length - 1) {
238
+ if (c >= contextLength || i == list.length - 1) {
178
239
  try {
179
- await translate(source, lang, model, translations, contextFile);
240
+ await translate(provider, source, lang, model, translations, contextFile, timeout);
180
241
  if (verbose) {
181
242
  translations.forEach((trans) => {
182
243
  console.log(trans.msgid);
@@ -187,7 +248,7 @@ export async function translatePo(model, po, source, lang, verbose, output, cont
187
248
  c = 0;
188
249
  // update progress
189
250
  printProgress(i + 1, list.length);
190
- // save po file after each 2000 characters
251
+ // save po file after each 2000 characters by default
191
252
  await compilePo(potrans, output || po, compileOptions);
192
253
  }
193
254
  catch (error) {
@@ -205,7 +266,7 @@ export async function translatePo(model, po, source, lang, verbose, output, cont
205
266
  else {
206
267
  console.error(error.message);
207
268
  if (error.code == "ECONNABORTED") {
208
- console.log('you may need to set "HTTPS_PROXY" to reach openai api.');
269
+ console.log('you may need to set "HTTPS_PROXY" to reach api.');
209
270
  }
210
271
  }
211
272
  }
@@ -213,13 +274,13 @@ export async function translatePo(model, po, source, lang, verbose, output, cont
213
274
  }
214
275
  console.log("done.");
215
276
  }
216
- export async function translatePoDir(model = "gpt-3.5-turbo", dir, source, lang, verbose, contextFile, compileOptions) {
277
+ export async function translatePoDir(provider, model, dir, source, lang, verbose, contextFile, contextLength, timeout, compileOptions) {
217
278
  const files = fs.readdirSync(dir);
218
279
  for (const file of files) {
219
280
  if (file.endsWith(".po")) {
220
281
  const po = path.join(dir, file);
221
282
  console.log(`translating ${po}`);
222
- await translatePo(model, po, source, lang, verbose, po, contextFile, compileOptions);
283
+ await translatePo(provider, model, po, source, lang, verbose, po, contextFile, contextLength, timeout, compileOptions);
223
284
  }
224
285
  }
225
286
  }
@@ -13,20 +13,25 @@ Translation guidelines are as follows:
13
13
  - Respond with the translated message wrapped in `<translated>` XML tags, including the same index attribute, e.g., `<translated index="1">`.
14
14
 
15
15
  4. **Context Handling**:
16
- - Some messages will include a context attribute in the translate tag, e.g., `<translate index="1" context="Menu">`.
16
+ - Some messages will include a context attribute in the `translate` tag, e.g., `<translate index="1" context="Menu">`.
17
17
  - Use this context to inform your translation but only return the translated text.
18
- - Example:
18
+ - Example (translating from `en_GB` to `fr_FR`):
19
19
  Input: `<translate index="1" context="Menu">File</translate>`
20
20
  Output: `<translated index="1">Fichier</translated>`
21
21
 
22
+ 5. **Translator notes**:
23
+ - Some messages will include translator notes attribute in the `translate` tag, e.g., `<translate index="2" notes="Placeholder is a person's name">`.
24
+ - These notes are for you, to aid you in achieving an accurate translation.
25
+ - Pay attention to these notes, but do not include them in your response, and never respond to the notes.
26
+
22
27
  5. **Multiple Translations**:
23
28
  - You may receive multiple translation requests in a single input, each with a unique index.
24
29
  - Ensuring the complete number of requests are translated, even if they are repeated, while maintaining the original order when possible.
25
30
 
26
- **Examples**:
31
+ **Example (translating from `en_US` to `zh_CN`):**
27
32
  - Input:
28
33
  `<translate index="1">This is a message. </translate>`
29
- `<translate index="2"> Hello %s</translate>`
34
+ `<translate index="2" notes="TRANSLATORS: Placeholder is a person's first name"> Hello %s</translate>`
30
35
  - Output:
31
36
  `<translated index="1">这是一条信息</translated>`
32
37
  `<translated index="2"> 你好 %s</translated>`
@@ -1,4 +1,4 @@
1
- import { GetTextTranslations } from "gettext-parser";
1
+ import { GetTextTranslations, GetTextPoCompilerOptions } from "gettext-parser";
2
2
  /**
3
3
  * copy source file to destination file if destination file does not exist
4
4
  * @param file destination file path
@@ -8,12 +8,7 @@ import { GetTextTranslations } from "gettext-parser";
8
8
  export declare function copyFileIfNotExists(file: string, copyFile: string, force?: boolean): void;
9
9
  export declare function openFileByDefault(filePath: string): void;
10
10
  export declare function parsePo(poFile: string, defaultCharset?: string): Promise<GetTextTranslations>;
11
- export type CompileOptions = {
12
- foldLength?: number;
13
- sort?: boolean | ((a: never, b: never) => number);
14
- escapeCharacters?: boolean;
15
- };
16
- export declare function compilePo(data: GetTextTranslations, poFile: string, options?: CompileOptions): Promise<void>;
11
+ export declare function compilePo(data: GetTextTranslations, poFile: string, options?: GetTextPoCompilerOptions): Promise<void>;
17
12
  export declare function printProgress(progress: number, total: number, extra?: string): void;
18
13
  export declare function gitRootDir(dir?: string): string | null;
19
14
  /**
package/lib/src/utils.js CHANGED
@@ -36,7 +36,7 @@ export function parsePo(poFile, defaultCharset) {
36
36
  fs.readFile(poFile, (err, buffer) => {
37
37
  if (err)
38
38
  reject(err);
39
- const result = po.parse(buffer, defaultCharset ?? "utf-8");
39
+ const result = po.parse(buffer, { defaultCharset: defaultCharset ?? "utf-8" });
40
40
  resolve(result);
41
41
  });
42
42
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gpt-po",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "command tool for translate po files by gpt",
5
5
  "main": "lib/src/index.js",
6
6
  "type": "module",
@@ -11,7 +11,7 @@
11
11
  "start": "node lib/src/index.js",
12
12
  "test": "echo \"Error: no test specified\" && exit 1",
13
13
  "prebuild": "rimraf ./lib",
14
- "build": "tsc",
14
+ "build": "tsc -p tsconfig.json",
15
15
  "postbuild": "ncp ./src/ ./lib/src/ \"--filter=^(?!.*\\.ts$).*$\"",
16
16
  "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
17
17
  "lint": "tslint -p tsconfig.json"
@@ -34,7 +34,7 @@
34
34
  "openai"
35
35
  ],
36
36
  "devDependencies": {
37
- "@types/gettext-parser": "^4.0.4",
37
+ "@types/gettext-parser": "^9.0.0",
38
38
  "@types/jest": "^29.5.12",
39
39
  "jest": "^29.7.0",
40
40
  "ncp": "^2.0.0",
@@ -46,11 +46,13 @@
46
46
  "typescript": "^5.5.4"
47
47
  },
48
48
  "dependencies": {
49
+ "@anthropic-ai/sdk": "^0.99.0",
50
+ "@google/genai": "^2.6.0",
49
51
  "commander": "^12.1.0",
50
- "gettext-parser": "^8.0.0",
51
- "openai": "^4.56.0"
52
+ "gettext-parser": "^9.0.1",
53
+ "openai": "^6.39.0"
52
54
  },
53
55
  "engines": {
54
- "node": ">=18.0.0"
56
+ "node": ">=20.0.0"
55
57
  }
56
58
  }