gpt-po 1.1.2 → 1.2.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.
@@ -0,0 +1 @@
1
+ buy_me_a_coffee: ryanhex
package/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## [1.2.0] - 2024-11-26
4
+ - Submit multiple entries per chat to boost speed and reduce tokens
5
+ - Command `systemprompt` has been removed
6
+ - Bug fixes
7
+
8
+ ## [1.1.2] - 2024-11-12
9
+ - Update command options for OpenAI model
10
+ - Dependency updates, target es2022, module(Nodejs 18+)
11
+
12
+ ## [1.0.11] - 2023-10-26
13
+ - Add `gpt-po remove` command
14
+ - improve prompt for translation
15
+
16
+ ## [1.0.9] - 2023-09-08
17
+ - Can add dictionaries for each languages
package/README.md CHANGED
@@ -7,6 +7,8 @@ Translation tool for gettext (po) files that supports custom system prompts and
7
7
 
8
8
  Read in other languages: English | [简体中文](./README_zh-CN.md)
9
9
 
10
+ <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>
11
+
10
12
  ## Installation
11
13
 
12
14
  ```
@@ -25,8 +27,6 @@ Set `OPENAI_API_KEY` before using this tool.
25
27
  - `gpt-po --dir .` Translate all po files in current directory to a designated target language.
26
28
  - `gpt-po userdict` Modify or view user dictionaries
27
29
  - `gpt-po userdict --explore` Explore user dictionaries, if you want add new dictionaries or modify existing dictionaries. dictionaries can be named as `dictionary-<lang>.json`, for example, `dictionary-zh.json` is the dictionary for Simplified Chinese.
28
- - `gpt-po systemprompt` Modify or view system prompts, if you are not sure how to use it, you can leave it alone
29
- - `gpt-po systemprompt --reset` Reset system prompts
30
30
 
31
31
  ```
32
32
  Usage: gpt-po [options] [command]
@@ -40,7 +40,6 @@ Options:
40
40
  Commands:
41
41
  translate [options] translate po file (default command)
42
42
  sync [options] update po from pot file
43
- systemprompt [options] open/edit system prompt
44
43
  userdict [options] open/edit user dictionary
45
44
  remove [options] remove po entries by options
46
45
  help [command] display help for command
@@ -81,3 +80,14 @@ Options:
81
80
  -rc, --reference-contains <text> remove entries whose reference contains text, text can be a regular expression like /text/ig
82
81
  -h, --help display help for command
83
82
  ```
83
+
84
+ ```
85
+ Usage: gpt-po sync [options]
86
+
87
+ update po from pot file
88
+
89
+ Options:
90
+ --po <file> po file path
91
+ --pot <file> pot file path
92
+ -h, --help display help for command
93
+ ```
package/README_zh-CN.md CHANGED
@@ -7,6 +7,8 @@ gettext(po)文件翻译工具,支持自定义系统提示词和用户字典,
7
7
 
8
8
  使用其他语言阅读:[English](./README.md) | 简体中文
9
9
 
10
+ <a href="https://buymeacoffee.com/ryanhex" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-red.png" alt="请我喝杯咖啡" height="41" width="174"></a>
11
+
10
12
  ## 安装
11
13
 
12
14
  ```
@@ -27,8 +29,6 @@ npm install gpt-po
27
29
  - `gpt-po --dir .` 将当前目录下的所有 po 文件翻译成目标语言。
28
30
  - `gpt-po userdict` 修改或查看用户词典。
29
31
  - `gpt-po userdict --explore` 浏览用户词典,如果您想添加新词典或修改现有词典,词典可以命名为 `dictionary-<lang>.json`,例如 `dictionary-zh.json` 是简体中文词典。
30
- - `gpt-po systemprompt` 修改或查看系统提示,如果您不确定如何使用它,可以忽略。
31
- - `gpt-po systemprompt --reset` 重置系统提示。
32
32
 
33
33
  ```
34
34
  用法: gpt-po [options] [command]
@@ -42,7 +42,6 @@ npm install gpt-po
42
42
  命令:
43
43
  translate [options] 翻译 po 文件(默认命令)
44
44
  sync [options] 根据 pot 文件更新 po 文件
45
- systemprompt [options] 打开/编辑系统提示
46
45
  userdict [options] 打开/编辑用户词典
47
46
  remove [options] 通过选项删除 po 条目
48
47
  help [command] 显示命令帮助
@@ -83,3 +82,14 @@ npm install gpt-po
83
82
  -rc, --reference-contains <text> 删除引用包含文本的条目,文本可以是正则表达式,例如 /text/ig
84
83
  -h, --help 显示命令帮助
85
84
  ```
85
+
86
+ ```
87
+ 用法: gpt-po sync [options]
88
+
89
+ 从 pot 文件中更新条目到 po 文件
90
+
91
+ Options:
92
+ --po <file> po 文件路径
93
+ --pot <file> pot 文件路径
94
+ -h, --help 显示命令帮助
95
+ ```
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gpt-po",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "description": "command tool for translate po files by gpt",
5
5
  "main": "lib/src/index.js",
6
6
  "type": "module",
@@ -1,2 +1,2 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node --no-warnings=ExperimentalWarning
2
2
  export {};
package/lib/src/index.js CHANGED
@@ -1,22 +1,38 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node --no-warnings=ExperimentalWarning
2
2
  import { Command, Option } from "commander";
3
3
  import path from "path";
4
4
  import { fileURLToPath } from "url";
5
5
  import pkg from "../package.json" with { type: "json" };
6
+ import { removeByOptions } from "./manipulate.js";
6
7
  import { sync } from "./sync.js";
7
8
  import { init, translatePo, translatePoDir } from "./translate.js";
8
- import { copyFileIfNotExists, compilePo, findConfig, openFileByDefault, openFileExplorer, parsePo } from "./utils.js";
9
- import { removeByOptions } from "./manipulate.js";
9
+ import { compilePo, copyFileIfNotExists, findConfig, openFileByDefault, openFileExplorer, parsePo } from "./utils.js";
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = path.dirname(__filename);
12
12
  const program = new Command();
13
13
  program.name(pkg.name).version(pkg.version).description(pkg.description);
14
- program
15
- .command("translate", { isDefault: true })
14
+ const getCompileOptions = (args) => {
15
+ const foldLength = args.poFoldLen === "false" ? 0 : parseInt(args.poFoldLen);
16
+ const sort = args.poSort;
17
+ const escapeCharacters = args.poEscChars;
18
+ if (isNaN(foldLength)) {
19
+ console.error("--po-fold-len must be a number or false");
20
+ process.exit(1);
21
+ }
22
+ return { foldLength, sort, escapeCharacters };
23
+ };
24
+ class SharedOptionsCommand extends Command {
25
+ addCompileOptions() {
26
+ return this.option("--po-fold-len <length>", "a gettext compile option, the length at which to fold message strings into newlines, set to 0 or false to disable folding", "120")
27
+ .option("--po-sort", "a gettext compile option, sort entries by msgid", false)
28
+ .option("--po-esc-chars", "a gettext compile option, escape characters in output, if false, will skip escape newlines and quotes characters functionality", true);
29
+ }
30
+ }
31
+ const translateCommand = new SharedOptionsCommand("translate")
16
32
  .description("translate po file (default command)")
17
33
  .addOption(new Option("-k, --key <key>", "openai api key").env("OPENAI_API_KEY"))
18
34
  .addOption(new Option("--host <host>", "openai api host").env("OPENAI_API_HOST"))
19
- .addOption(new Option("--model <model>", "openai model").env("OPENAI_MODEL").default("gpt-4o"))
35
+ .addOption(new Option("--model <model>", "openai model").env("OPENAI_MODEL").default("gpt-4o-mini"))
20
36
  .addOption(new Option("--po <file>", "po file path").conflicts("dir"))
21
37
  .addOption(new Option("--dir <dir>", "po file directory").conflicts("po"))
22
38
  .option("-src, --source <lang>", "source language (ISO 639-1)", "en")
@@ -24,6 +40,7 @@ program
24
40
  .option("--verbose", "print verbose log")
25
41
  .option("--context <file>", "text file that provides the bot additional context")
26
42
  .addOption(new Option("-o, --output <file>", "output file path, overwirte po file by default").conflicts("dir"))
43
+ .addCompileOptions()
27
44
  .action(async (args) => {
28
45
  const { key, host, model, po, dir, source, lang, verbose, output, context } = args;
29
46
  if (host) {
@@ -38,63 +55,52 @@ program
38
55
  process.exit(1);
39
56
  }
40
57
  init();
58
+ const compileOptions = getCompileOptions(args);
41
59
  if (po) {
42
- await translatePo(model, po, source, lang, verbose, output, context);
60
+ await translatePo(model, po, source, lang, verbose, output, context, compileOptions);
43
61
  }
44
62
  else if (dir) {
45
- await translatePoDir(model, dir, source, lang, verbose, context);
63
+ await translatePoDir(model, dir, source, lang, verbose, context, compileOptions);
46
64
  }
47
65
  else {
48
66
  console.error("po file or directory is required");
49
67
  process.exit(1);
50
68
  }
51
69
  });
52
- program
53
- .command("sync")
70
+ program.addCommand(translateCommand, { isDefault: true });
71
+ const syncCommand = new SharedOptionsCommand("sync")
54
72
  .description("update po from pot file")
55
73
  .requiredOption("--po <file>", "po file path")
56
74
  .requiredOption("--pot <file>", "pot file path")
57
- .action(async ({ po, pot }) => {
58
- await sync(po, pot);
59
- });
60
- // program command `systemprompt` with help text `open/edit system prompt`
61
- program
62
- .command("systemprompt")
63
- .description("open/edit system prompt")
64
- .option("--reset", "reset system prompt to default")
65
- .action((args) => {
66
- const { reset } = args;
67
- // open `systemprompt.txt` file by system text default editor
68
- const copyFile = __dirname + "/systemprompt.txt";
69
- // user home path
70
- const promptFile = findConfig("systemprompt.txt");
71
- copyFileIfNotExists(promptFile, copyFile, reset);
72
- if (reset) {
73
- console.log("systemprompt.txt reset to default");
74
- }
75
- openFileByDefault(promptFile);
75
+ .option("-o, --output <file>", "output file path, overwirte po file by default")
76
+ .addCompileOptions()
77
+ .action(async (args) => {
78
+ const { po, pot } = args;
79
+ await sync(po, pot, getCompileOptions(args));
76
80
  });
81
+ program.addCommand(syncCommand);
77
82
  // program command `userdict` with help text `open/edit user dictionary`
78
83
  program
79
84
  .command("userdict")
80
85
  .description("open/edit user dictionary")
81
86
  .option("--explore", "open user dictionary directory")
87
+ .option("-l, --lang <lang>", "target language (ISO 639-1)")
82
88
  .action((args) => {
83
- const { explore } = args;
89
+ const { explore, lang } = args;
84
90
  // open `dictionary.json` file by system text default editor
85
91
  const copyFile = __dirname + "/dictionary.json";
86
- // user home path
87
- const dictFile = findConfig("dictionary.json");
92
+ // find from user home path
93
+ const dictFile = findConfig(`dictionary${lang ? "-" + lang : ""}.json`);
88
94
  if (explore) {
89
95
  // open user dictionary directory
90
96
  return openFileExplorer(dictFile);
91
97
  }
92
- copyFileIfNotExists(dictFile, copyFile);
98
+ if (!lang)
99
+ copyFileIfNotExists(dictFile, copyFile);
93
100
  openFileByDefault(dictFile);
94
101
  });
95
102
  // program command `remove` with help text `remove po entries by options`
96
- program
97
- .command("remove")
103
+ const removeCommand = new SharedOptionsCommand("remove")
98
104
  .description("remove po entries by options")
99
105
  .requiredOption("--po <file>", "po file path")
100
106
  .option("--fuzzy", "remove fuzzy entries")
@@ -104,7 +110,10 @@ program
104
110
  .option("-tnf, --translated-not-fuzzy", "remove translated not fuzzy entries")
105
111
  .option("-ft, --fuzzy-translated", "remove fuzzy translated entries")
106
112
  .option("-rc, --reference-contains <text>", "remove entries whose reference contains text, text can be a regular expression like /text/ig")
113
+ .option("-o, --output <file>", "output file path, overwirte po file by default")
114
+ .addCompileOptions()
107
115
  .action(async (args) => {
116
+ this;
108
117
  const { po, fuzzy, obsolete, untranslated, translated, translatedNotFuzzy, fuzzyTranslated, referenceContains } = args;
109
118
  const options = {
110
119
  fuzzy,
@@ -113,12 +122,13 @@ program
113
122
  translated,
114
123
  translatedNotFuzzy,
115
124
  fuzzyTranslated,
116
- referenceContains,
125
+ referenceContains
117
126
  };
118
127
  const output = args.output || po;
119
128
  const translations = await parsePo(po);
120
- await compilePo(removeByOptions(translations, options), output);
129
+ await compilePo(removeByOptions(translations, options), output, getCompileOptions(args));
121
130
  console.log("done");
122
131
  });
132
+ program.addCommand(removeCommand);
123
133
  program.parse(process.argv);
124
134
  //# sourceMappingURL=index.js.map
package/lib/src/sync.d.ts CHANGED
@@ -1 +1,2 @@
1
- export declare function sync(po: string, pot: string): Promise<void>;
1
+ import { CompileOptions } from "./utils.js";
2
+ export declare function sync(po: string, pot: string, compileOptions?: CompileOptions): Promise<void>;
package/lib/src/sync.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { compilePo, parsePo } from "./utils.js";
2
- export async function sync(po, pot) {
2
+ export async function sync(po, pot, compileOptions) {
3
3
  const potrans = await parsePo(po);
4
4
  const potrans2 = await parsePo(pot);
5
5
  for (const [ctx, entries] of Object.entries(potrans2.translations)) {
@@ -13,6 +13,6 @@ export async function sync(po, pot) {
13
13
  }
14
14
  }
15
15
  potrans.translations = potrans2.translations;
16
- await compilePo(potrans, po);
16
+ await compilePo(potrans, po, compileOptions);
17
17
  }
18
18
  //# sourceMappingURL=sync.js.map
@@ -1,31 +1 @@
1
- You are a language translation expert. You will translate text from one language to another, following these guidelines:
2
-
3
- 1. **Language Codes**:
4
- - If provided with an ISO 639 two-letter language code (e.g., `de`), use the language’s main dialect (e.g., `de` is equivalent to `de_DE`).
5
- - If provided with both an ISO 639 language code and an ISO 3166 country code (e.g., `es_MX`), translate into that specific regional dialect.
6
-
7
- 2. **Placeholder Handling**:
8
- - Maintain the positions of placeholders (e.g., %s, %d, {example}) in the translated text. Do not translate placeholders.
9
-
10
- 3. **Formatting**:
11
- - Preserve the formatting of untranslatable portions.
12
- - Retain any whitespace at the beginning or end of the message.
13
- - Add or omit a period (.) at the end of your translation to match the incoming message.
14
-
15
- 4. **XML Tags**:
16
- - Input messages will be wrapped in `<translate>` XML tags.
17
- - Respond with the translated message wrapped in `<translated>` XML tags.
18
-
19
- 5. **Quality**:
20
- - Translate in a colloquial, professional, and elegant manner without sounding like a machine translation.
21
-
22
- 6. **Error Handling**:
23
- - If you cannot reliably translate a message, respond without `<translated>` XML tags and provide a short reason why.
24
-
25
- **Examples**:
26
- - Input: `<translate>This is a message. </translate>`
27
- - Output: `<translated>Este es un mensaje. </translated>`
28
- - Input: `<translate> Hello %s</translate>`
29
- - Output: `<translated> Hola %s</translated>`
30
-
31
- Do not answer questions or explain concepts. Only provide translations within `<translated>` XML tags unless you need to respond with a short error reason.
1
+ You are a language translation expert. You will carefully follow the translation guidelines to translate the incoming XML messages from one language to another.
@@ -1,6 +1,7 @@
1
1
  import { GetTextTranslation } from "gettext-parser";
2
2
  import { OpenAI } from "openai";
3
+ import { CompileOptions } from "./utils.js";
3
4
  export declare function init(force?: boolean): OpenAI;
4
- export declare function translate(text: string, src: string, lang: string, model: string, comments: GetTextTranslation["comments"] | undefined, contextFile: string): import("openai/core.mjs").APIPromise<OpenAI.Chat.Completions.ChatCompletion>;
5
- export declare function translatePo(model: string, po: string, source: string, lang: string, verbose: boolean, output: string, contextFile: string): Promise<void>;
6
- export declare function translatePoDir(model: string | undefined, dir: string, source: string, lang: string, verbose: boolean, contextFile: string): Promise<void>;
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>;
@@ -8,47 +8,59 @@ const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
9
9
  let _openai;
10
10
  let _systemprompt;
11
+ let _userprompt;
11
12
  let _userdict;
12
13
  export function init(force) {
13
14
  if (!_openai || force) {
14
15
  let configuration = {
15
- apiKey: process.env.OPENAI_API_KEY,
16
+ apiKey: process.env.OPENAI_API_KEY
16
17
  };
17
18
  _openai = new OpenAI(configuration);
18
19
  if (process.env.OPENAI_API_HOST) {
19
20
  _openai.baseURL = process.env.OPENAI_API_HOST.replace(/\/+$/, "") + "/v1";
20
21
  }
21
22
  }
22
- // load systemprompt.txt from homedir
23
+ // load systemprompt.txt from project
23
24
  if (!_systemprompt || force) {
24
- const systemprompt = findConfig("systemprompt.txt");
25
- copyFileIfNotExists(systemprompt, path.join(__dirname, "systemprompt.txt"));
26
- _systemprompt = fs.readFileSync(systemprompt, "utf-8");
25
+ _systemprompt = fs.readFileSync(path.join(__dirname, "systemprompt.txt"), "utf-8");
26
+ }
27
+ // load userprompt.txt from project
28
+ if (!_userprompt || force) {
29
+ _userprompt = fs.readFileSync(path.join(__dirname, "userprompt.txt"), "utf-8");
27
30
  }
28
31
  // load dictionary.json from homedir
29
32
  if (!_userdict || force) {
30
33
  const userdict = findConfig("dictionary.json");
31
34
  copyFileIfNotExists(userdict, path.join(__dirname, "dictionary.json"));
32
- _userdict = { "default": JSON.parse(fs.readFileSync(userdict, "utf-8")) };
35
+ _userdict = { default: JSON.parse(fs.readFileSync(userdict, "utf-8")) };
33
36
  }
34
37
  return _openai;
35
38
  }
36
- export function translate(text, src, lang, model, comments, contextFile) {
37
- const lang_code = lang.toLowerCase().trim().replace(/[\W_]+/g, "-");
38
- const dicts = Object.entries(_userdict[lang_code] || _userdict["default"])
39
- .filter(([k, _]) => text.toLowerCase().includes(k.toLowerCase()))
40
- .map(([k, v]) => [
41
- { role: "user", content: k },
42
- { role: "assistant", content: v },
43
- ])
44
- .flat();
45
- var notes = "";
46
- if (comments != undefined && comments.extracted != undefined)
47
- notes = comments.extracted;
48
- var context = "";
49
- if (contextFile !== undefined)
50
- context = "\n\n" + fs.readFileSync(contextFile, "utf-8");
51
- return _openai.chat.completions.create({
39
+ export async function translate(src, lang, model, translations, contextFile) {
40
+ const lang_code = lang
41
+ .toLowerCase()
42
+ .trim()
43
+ .replace(/[\W_]+/g, "-");
44
+ const dicts = Object.entries(_userdict[lang_code] || _userdict["default"]).reduce((acc, [k, v], idx) => {
45
+ if (translations.some((tr) => tr.msgid.toLowerCase().includes(k.toLowerCase()))) {
46
+ acc.user.push(`<translate index="${idx + 1}">${k}</translate>`);
47
+ acc.assistant.push(`<translated index="${idx + 1}">${v}</translated>`);
48
+ }
49
+ return acc;
50
+ }, { 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");
59
+ const context = contextFile ? "\n\nContext: " + fs.readFileSync(contextFile, "utf-8") : "";
60
+ const translationsContent = translations
61
+ .map((tr, idx) => `<translate index="${idx + dicts.user.length + 1}">${tr.msgid}</translate>`)
62
+ .join("\n");
63
+ const res = await _openai.chat.completions.create({
52
64
  model: model,
53
65
  temperature: 0.1,
54
66
  messages: [
@@ -58,24 +70,44 @@ export function translate(text, src, lang, model, comments, contextFile) {
58
70
  },
59
71
  {
60
72
  role: "user",
61
- content: `Wait for my incoming message in "${src}" and translate it into "${lang}", carefully following your system prompt. ` + notes
73
+ content: `${_userprompt}\n\nWait for my incoming message in "${src}" and translate it into "${lang}"(a language code and an optional region code). ` +
74
+ notes
62
75
  },
63
76
  {
64
77
  role: "assistant",
65
- content: `Understood, I will translate your incoming "${src}" message into "${lang}", carefully following my system prompt. Please go ahead and send your message for translation.`
78
+ content: `Understood, I will translate your incoming "${src}" message into "${lang}", carefully following guidelines. Please go ahead and send your message for translation.`
66
79
  },
67
- // add userdict here
68
- ...dicts,
80
+ // add userdict
81
+ ...(dicts.user.length > 0
82
+ ? [
83
+ { role: "user", content: dicts.user.join("\n") },
84
+ { role: "assistant", content: dicts.assistant.join("\n") }
85
+ ]
86
+ : []),
87
+ // add user translations
69
88
  {
70
89
  role: "user",
71
- content: "<translate>" + text + "</translate>"
72
- },
73
- ],
90
+ content: translationsContent
91
+ }
92
+ ]
74
93
  }, {
75
94
  timeout: 20000,
95
+ stream: false
96
+ });
97
+ const content = res.choices[0].message.content ?? "";
98
+ translations.forEach((trans, idx) => {
99
+ const tag = `<translated index="${idx + dicts.user.length + 1}">`;
100
+ const s = content.indexOf(tag);
101
+ if (s > -1) {
102
+ const e = content.indexOf("</translated>", s);
103
+ trans.msgstr[0] = content.slice(s + tag.length, e);
104
+ }
105
+ else {
106
+ console.error("Error: Unable to find translation for string [" + trans.msgid + "]");
107
+ }
76
108
  });
77
109
  }
78
- export async function translatePo(model, po, source, lang, verbose, output, contextFile) {
110
+ export async function translatePo(model, po, source, lang, verbose, output, contextFile, compileOptions) {
79
111
  const potrans = await parsePo(po);
80
112
  if (!lang)
81
113
  lang = potrans.headers["Language"];
@@ -84,7 +116,10 @@ export async function translatePo(model, po, source, lang, verbose, output, cont
84
116
  return;
85
117
  }
86
118
  // try to load dictionary by lang-code if it not loaded
87
- const lang_code = lang.toLowerCase().trim().replace(/[\W_]+/g, "-");
119
+ const lang_code = lang
120
+ .toLowerCase()
121
+ .trim()
122
+ .replace(/[\W_]+/g, "-");
88
123
  if (!_userdict[lang_code]) {
89
124
  const lang_dic_file = findConfig(`dictionary-${lang_code}.json`);
90
125
  if (fs.existsSync(lang_dic_file)) {
@@ -110,16 +145,16 @@ export async function translatePo(model, po, source, lang, verbose, output, cont
110
145
  }
111
146
  }
112
147
  if (trimed) {
113
- await compilePo(potrans, po);
148
+ await compilePo(potrans, po, compileOptions);
114
149
  }
115
150
  if (list.length == 0) {
116
151
  console.log("done.");
117
152
  return;
118
153
  }
119
154
  potrans.headers["Last-Translator"] = `gpt-po v${pkg.version}`;
155
+ const translations = [];
120
156
  let err429 = false;
121
- let modified = false;
122
- for (let i = 0; i < list.length; i++) {
157
+ for (let i = 0, c = 0; i < list.length; i++) {
123
158
  if (i == 0)
124
159
  printProgress(i, list.length);
125
160
  if (err429) {
@@ -127,53 +162,56 @@ export async function translatePo(model, po, source, lang, verbose, output, cont
127
162
  await new Promise((resolve) => setTimeout(resolve, 20000));
128
163
  }
129
164
  const trans = list[i];
130
- try {
131
- const res = await translate(trans.msgid, source, lang, model, trans.comments, contextFile);
132
- var translated = res.choices[0].message?.content || trans.msgstr[0];
133
- if (!translated.startsWith('<translated>') && !translated.endsWith('</translated>')) {
134
- // We got an error response
135
- console.log("Error: Unable to translate string [" + trans.msgid + "]. Bot says [" + translated + "]");
136
- continue;
137
- }
138
- // We got a valid translation response
139
- trans.msgstr[0] = translated.replace('<translated>', '').replace('</translated>', '');
140
- modified = true;
141
- if (verbose) {
142
- console.log(trans.msgid);
143
- console.log(trans.msgstr[0]);
144
- }
145
- printProgress(i + 1, list.length);
146
- await compilePo(potrans, output || po);
165
+ if (c < 2000) {
166
+ translations.push(trans);
167
+ c += trans.msgid.length;
147
168
  }
148
- catch (error) {
149
- if (error.response) {
150
- if (error.response.status == 429) {
151
- // caused by rate limit exceeded, should sleep for 20 seconds.
152
- err429 = true;
153
- --i;
154
- }
155
- else {
156
- console.error(error.response.status);
157
- console.log(error.response.data);
169
+ if (c >= 2000 || i == list.length - 1) {
170
+ try {
171
+ await translate(source, lang, model, translations, contextFile);
172
+ if (verbose) {
173
+ translations.forEach((trans) => {
174
+ console.log(trans.msgid);
175
+ console.log(trans.msgstr[0]);
176
+ });
158
177
  }
178
+ translations.length = 0;
179
+ c = 0;
180
+ // update progress
181
+ printProgress(i + 1, list.length);
182
+ // save po file after each 2000 characters
183
+ await compilePo(potrans, output || po, compileOptions);
159
184
  }
160
- else {
161
- console.error(error.message);
162
- if (error.code == "ECONNABORTED") {
163
- console.log('you may need to set "HTTPS_PROXY" to reach openai api.');
185
+ catch (error) {
186
+ if (error.response) {
187
+ if (error.response.status == 429) {
188
+ // caused by rate limit exceeded, should sleep for 20 seconds.
189
+ err429 = true;
190
+ --i;
191
+ }
192
+ else {
193
+ console.error(error.response.status);
194
+ console.log(error.response.data);
195
+ }
196
+ }
197
+ else {
198
+ console.error(error.message);
199
+ if (error.code == "ECONNABORTED") {
200
+ console.log('you may need to set "HTTPS_PROXY" to reach openai api.');
201
+ }
164
202
  }
165
203
  }
166
204
  }
167
205
  }
168
206
  console.log("done.");
169
207
  }
170
- export async function translatePoDir(model = "gpt-3.5-turbo", dir, source, lang, verbose, contextFile) {
208
+ export async function translatePoDir(model = "gpt-3.5-turbo", dir, source, lang, verbose, contextFile, compileOptions) {
171
209
  const files = fs.readdirSync(dir);
172
210
  for (const file of files) {
173
211
  if (file.endsWith(".po")) {
174
212
  const po = path.join(dir, file);
175
213
  console.log(`translating ${po}`);
176
- await translatePo(model, po, source, lang, verbose, po, contextFile);
214
+ await translatePo(model, po, source, lang, verbose, po, contextFile, compileOptions);
177
215
  }
178
216
  }
179
217
  }
@@ -0,0 +1,27 @@
1
+ Translation guidelines are as follows:
2
+
3
+ 1. **Placeholder Handling**:
4
+ - Maintain the positions of placeholders (e.g., %s, %d, {example}) in the translated text. Do not translate placeholders.
5
+
6
+ 2. **Formatting**:
7
+ - Preserve the formatting of untranslatable portions.
8
+ - Retain any whitespace at the beginning or end of the message.
9
+ - Add or omit a period (.) at the end of your translation to match the incoming message.
10
+
11
+ 3. **XML Tags and Indexing**:
12
+ - Input messages will be wrapped in `<translate>` XML tags with an index attribute, e.g., `<translate index="1">`.
13
+ - Respond with the translated message wrapped in `<translated>` XML tags, including the same index attribute, e.g., `<translated index="1">`.
14
+
15
+ 4. **Multiple Translations**:
16
+ - You may receive multiple translation requests in a single input, each with a unique index.
17
+ - Ensuring the complete number of requests are translated, even if they are repeated, while maintaining the original order when possible.
18
+
19
+ **Examples**:
20
+ - Input:
21
+ `<translate index="1">This is a message. </translate>`
22
+ `<translate index="2"> Hello %s</translate>`
23
+ - Output:
24
+ `<translated index="1">这是一条信息</translated>`
25
+ `<translated index="2"> 你好 %s</translated>`
26
+
27
+ Do not answer questions or explain concepts. Only provide translations within `<translated>` XML tags unless you need to respond with a short error reason.
@@ -8,7 +8,12 @@ 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 declare function compilePo(data: GetTextTranslations, poFile: string): Promise<void>;
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>;
12
17
  export declare function printProgress(progress: number, total: number, extra?: string): void;
13
18
  export declare function gitRootDir(dir?: string): string | null;
14
19
  /**
package/lib/src/utils.js CHANGED
@@ -36,13 +36,13 @@ export function parsePo(poFile, defaultCharset) {
36
36
  fs.readFile(poFile, (err, buffer) => {
37
37
  if (err)
38
38
  reject(err);
39
- var result = po.parse(buffer, defaultCharset ?? "utf-8");
39
+ const result = po.parse(buffer, defaultCharset ?? "utf-8");
40
40
  resolve(result);
41
41
  });
42
42
  });
43
43
  }
44
- export function compilePo(data, poFile) {
45
- const buffer = po.compile(data, { foldLength: 120 });
44
+ export function compilePo(data, poFile, options = { foldLength: 120, sort: false, escapeCharacters: true }) {
45
+ const buffer = po.compile(data, options);
46
46
  return new Promise((resolve, reject) => {
47
47
  fs.writeFile(poFile, buffer, (err) => {
48
48
  if (err)
@@ -95,7 +95,7 @@ export function findConfig(fileName) {
95
95
  path.join(currentDir, ".gpt-po", fileName),
96
96
  path.join(currentDir, fileName),
97
97
  path.join(gitDir, ".gpt-po", fileName),
98
- path.join(homeDir, ".gpt-po", fileName)
98
+ path.join(homeDir, ".gpt-po", fileName),
99
99
  ];
100
100
  // check if file exists and return the first one
101
101
  for (const filePath of filePaths) {
@@ -111,10 +111,10 @@ export function findConfig(fileName) {
111
111
  * @param location folder or file path
112
112
  */
113
113
  export function openFileExplorer(location) {
114
- if (platform() === 'win32') {
114
+ if (platform() === "win32") {
115
115
  exec(`explorer.exe "${path.dirname(location)}"`);
116
116
  }
117
- else if (platform() === 'darwin') {
117
+ else if (platform() === "darwin") {
118
118
  exec(`open "${path.dirname(location)}"`);
119
119
  }
120
120
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gpt-po",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "description": "command tool for translate po files by gpt",
5
5
  "main": "lib/src/index.js",
6
6
  "type": "module",