From 66878806148f086df4705d8c1e844f582359cf30 Mon Sep 17 00:00:00 2001 From: ryanhex53 Date: Tue, 26 Nov 2024 10:37:46 +0800 Subject: [PATCH] version 1.2.0, boost speed and reduce tokens --- .github/FUNDING.yml | 1 + .prettierrc | 5 +- CHANGELOG.md | 17 ++++ README.md | 16 +++- README_zh-CN.md | 16 +++- package-lock.json | 15 ++-- package.json | 2 +- src/index.ts | 110 +++++++++++++------------- src/manipulate.ts | 3 +- src/systemprompt.txt | 32 +------- src/translate.ts | 183 ++++++++++++++++++++++++++----------------- src/userprompt.txt | 27 +++++++ src/utils.ts | 28 ++++--- 13 files changed, 270 insertions(+), 185 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 CHANGELOG.md create mode 100644 src/userprompt.txt diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b6b8127 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +buy_me_a_coffee: ryanhex \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index f36e365..c7bb5c2 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,8 @@ { "tabWidth": 2, "useTabs": false, - "printWidth": 100 + "quoteProps": "preserve", + "printWidth": 110, + "trailingComma": "none", + "semi": true } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2a22a14 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +## [1.2.0] - 2024-11-26 +- Submit multiple entries per chat to boost speed and reduce tokens +- Command `systemprompt` has been removed +- Bug fixes + +## [1.1.2] - 2024-11-12 +- Update command options for OpenAI model +- Dependency updates, target es2022, module(Nodejs 18+) + +## [1.0.11] - 2023-10-26 +- Add `gpt-po remove` command +- improve prompt for translation + +## [1.0.9] - 2023-09-08 +- Can add dictionaries for each languages \ No newline at end of file diff --git a/README.md b/README.md index 6ca9948..4e9342e 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ Translation tool for gettext (po) files that supports custom system prompts and Read in other languages: English | [简体中文](./README_zh-CN.md) +Buy Me A Coffee + ## Installation ``` @@ -25,8 +27,6 @@ Set `OPENAI_API_KEY` before using this tool. - `gpt-po --dir .` Translate all po files in current directory to a designated target language. - `gpt-po userdict` Modify or view user dictionaries - `gpt-po userdict --explore` Explore user dictionaries, if you want add new dictionaries or modify existing dictionaries. dictionaries can be named as `dictionary-.json`, for example, `dictionary-zh.json` is the dictionary for Simplified Chinese. -- `gpt-po systemprompt` Modify or view system prompts, if you are not sure how to use it, you can leave it alone -- `gpt-po systemprompt --reset` Reset system prompts ``` Usage: gpt-po [options] [command] @@ -40,7 +40,6 @@ Options: Commands: translate [options] translate po file (default command) sync [options] update po from pot file - systemprompt [options] open/edit system prompt userdict [options] open/edit user dictionary remove [options] remove po entries by options help [command] display help for command @@ -81,3 +80,14 @@ Options: -rc, --reference-contains remove entries whose reference contains text, text can be a regular expression like /text/ig -h, --help display help for command ``` + +``` +Usage: gpt-po sync [options] + +update po from pot file + +Options: + --po po file path + --pot pot file path + -h, --help display help for command +``` \ No newline at end of file diff --git a/README_zh-CN.md b/README_zh-CN.md index 853c1ad..f4d5eb4 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -7,6 +7,8 @@ gettext(po)文件翻译工具,支持自定义系统提示词和用户字典, 使用其他语言阅读:[English](./README.md) | 简体中文 +请我喝杯咖啡 + ## 安装 ``` @@ -27,8 +29,6 @@ npm install gpt-po - `gpt-po --dir .` 将当前目录下的所有 po 文件翻译成目标语言。 - `gpt-po userdict` 修改或查看用户词典。 - `gpt-po userdict --explore` 浏览用户词典,如果您想添加新词典或修改现有词典,词典可以命名为 `dictionary-.json`,例如 `dictionary-zh.json` 是简体中文词典。 -- `gpt-po systemprompt` 修改或查看系统提示,如果您不确定如何使用它,可以忽略。 -- `gpt-po systemprompt --reset` 重置系统提示。 ``` 用法: gpt-po [options] [command] @@ -42,7 +42,6 @@ npm install gpt-po 命令: translate [options] 翻译 po 文件(默认命令) sync [options] 根据 pot 文件更新 po 文件 - systemprompt [options] 打开/编辑系统提示 userdict [options] 打开/编辑用户词典 remove [options] 通过选项删除 po 条目 help [command] 显示命令帮助 @@ -83,3 +82,14 @@ npm install gpt-po -rc, --reference-contains 删除引用包含文本的条目,文本可以是正则表达式,例如 /text/ig -h, --help 显示命令帮助 ``` + +``` +用法: gpt-po sync [options] + +从 pot 文件中更新条目到 po 文件 + +Options: + --po po 文件路径 + --pot pot 文件路径 + -h, --help 显示命令帮助 +``` diff --git a/package-lock.json b/package-lock.json index 526d2a5..d05fb83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gpt-po", - "version": "1.1.1", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gpt-po", - "version": "1.1.1", + "version": "1.2.0", "license": "MIT", "dependencies": { "commander": "^12.1.0", @@ -14,7 +14,7 @@ "openai": "^4.56.0" }, "bin": { - "gpt-po": "lib/src/wrapper.js" + "gpt-po": "lib/src/index.js" }, "devDependencies": { "@types/gettext-parser": "^4.0.4", @@ -29,7 +29,7 @@ "typescript": "^5.5.4" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, "node_modules/@ampproject/remapping": { @@ -2151,10 +2151,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://mirrors.tencent.com/npm/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", diff --git a/package.json b/package.json index d2ef86d..16178dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gpt-po", - "version": "1.1.2", + "version": "1.2.0", "description": "command tool for translate po files by gpt", "main": "lib/src/index.js", "type": "module", diff --git a/src/index.ts b/src/index.ts index 83906fc..ac1dcaa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env node --no-warnings=ExperimentalWarning import { Command, Option } from "commander"; import path from "path"; @@ -6,7 +6,14 @@ import { fileURLToPath } from "url"; import pkg from "../package.json" with { type: "json" }; import { sync } from "./sync.js"; import { init, translatePo, translatePoDir } from "./translate.js"; -import { copyFileIfNotExists, compilePo, findConfig, openFileByDefault, openFileExplorer, parsePo } from "./utils.js"; +import { + copyFileIfNotExists, + compilePo, + findConfig, + openFileByDefault, + openFileExplorer, + parsePo +} from "./utils.js"; import { removeByOptions } from "./manipulate.js"; const __filename = fileURLToPath(import.meta.url); @@ -29,9 +36,7 @@ program .option("--verbose", "print verbose log") .option("--context ", "text file that provides the bot additional context") .addOption( - new Option("-o, --output ", "output file path, overwirte po file by default").conflicts( - "dir", - ), + new Option("-o, --output ", "output file path, overwirte po file by default").conflicts("dir") ) .action(async (args) => { const { key, host, model, po, dir, source, lang, verbose, output, context } = args; @@ -66,70 +71,65 @@ program await sync(po, pot); }); -// program command `systemprompt` with help text `open/edit system prompt` -program - .command("systemprompt") - .description("open/edit system prompt") - .option("--reset", "reset system prompt to default") - .action((args) => { - const { reset } = args; - // open `systemprompt.txt` file by system text default editor - const copyFile = __dirname + "/systemprompt.txt"; - // user home path - const promptFile = findConfig("systemprompt.txt"); - copyFileIfNotExists(promptFile, copyFile, reset); - if (reset) { - console.log("systemprompt.txt reset to default"); - } - openFileByDefault(promptFile); - }); - // program command `userdict` with help text `open/edit user dictionary` program .command("userdict") .description("open/edit user dictionary") .option("--explore", "open user dictionary directory") + .option("-l, --lang ", "target language (ISO 639-1)") .action((args) => { - const { explore } = args; + const { explore, lang } = args; // open `dictionary.json` file by system text default editor const copyFile = __dirname + "/dictionary.json"; - // user home path - const dictFile = findConfig("dictionary.json"); + // find from user home path + const dictFile = findConfig(`dictionary${lang ? "-" + lang : ""}.json`); if (explore) { // open user dictionary directory return openFileExplorer(dictFile); } - copyFileIfNotExists(dictFile, copyFile); + if (!lang) copyFileIfNotExists(dictFile, copyFile); openFileByDefault(dictFile); }); - // program command `remove` with help text `remove po entries by options` - program - .command("remove") - .description("remove po entries by options") - .requiredOption("--po ", "po file path") - .option("--fuzzy", "remove fuzzy entries") - .option("-obs, --obsolete", "remove obsolete entries") - .option("-ut, --untranslated", "remove untranslated entries") - .option("-t, --translated", "remove translated entries") - .option("-tnf, --translated-not-fuzzy", "remove translated not fuzzy entries") - .option("-ft, --fuzzy-translated", "remove fuzzy translated entries") - .option("-rc, --reference-contains ", "remove entries whose reference contains text, text can be a regular expression like /text/ig") - .action(async (args) => { - const { po, fuzzy, obsolete, untranslated, translated, translatedNotFuzzy, fuzzyTranslated, referenceContains } = args; - const options = { - fuzzy, - obsolete, - untranslated, - translated, - translatedNotFuzzy, - fuzzyTranslated, - referenceContains, - }; - const output = args.output || po; - const translations = await parsePo(po); - await compilePo(removeByOptions(translations, options), output); - console.log("done") - }); +// program command `remove` with help text `remove po entries by options` +program + .command("remove") + .description("remove po entries by options") + .requiredOption("--po ", "po file path") + .option("--fuzzy", "remove fuzzy entries") + .option("-obs, --obsolete", "remove obsolete entries") + .option("-ut, --untranslated", "remove untranslated entries") + .option("-t, --translated", "remove translated entries") + .option("-tnf, --translated-not-fuzzy", "remove translated not fuzzy entries") + .option("-ft, --fuzzy-translated", "remove fuzzy translated entries") + .option( + "-rc, --reference-contains ", + "remove entries whose reference contains text, text can be a regular expression like /text/ig" + ) + .action(async (args) => { + const { + po, + fuzzy, + obsolete, + untranslated, + translated, + translatedNotFuzzy, + fuzzyTranslated, + referenceContains + } = args; + const options = { + fuzzy, + obsolete, + untranslated, + translated, + translatedNotFuzzy, + fuzzyTranslated, + referenceContains + }; + const output = args.output || po; + const translations = await parsePo(po); + await compilePo(removeByOptions(translations, options), output); + console.log("done"); + }); program.parse(process.argv); diff --git a/src/manipulate.ts b/src/manipulate.ts index f71367b..cf50226 100644 --- a/src/manipulate.ts +++ b/src/manipulate.ts @@ -15,7 +15,7 @@ export interface RemoveByOptions { */ export function removeByOptions( potrans: GetTextTranslations, - options: RemoveByOptions | undefined, + options: RemoveByOptions | undefined ): GetTextTranslations { const fuzzyRegx = /\bfuzzy\b/; const obsoleteRegx = /\bobsolete\b/; @@ -60,7 +60,6 @@ export function removeByOptions( delete potrans.translations[ctx][msgid]; } } - } } return potrans; diff --git a/src/systemprompt.txt b/src/systemprompt.txt index b05c21d..bcc336d 100644 --- a/src/systemprompt.txt +++ b/src/systemprompt.txt @@ -1,31 +1 @@ -You are a language translation expert. You will translate text from one language to another, following these guidelines: - -1. **Language Codes**: - - 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`). - - 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. - -2. **Placeholder Handling**: - - Maintain the positions of placeholders (e.g., %s, %d, {example}) in the translated text. Do not translate placeholders. - -3. **Formatting**: - - Preserve the formatting of untranslatable portions. - - Retain any whitespace at the beginning or end of the message. - - Add or omit a period (.) at the end of your translation to match the incoming message. - -4. **XML Tags**: - - Input messages will be wrapped in `` XML tags. - - Respond with the translated message wrapped in `` XML tags. - -5. **Quality**: - - Translate in a colloquial, professional, and elegant manner without sounding like a machine translation. - -6. **Error Handling**: - - If you cannot reliably translate a message, respond without `` XML tags and provide a short reason why. - -**Examples**: -- Input: `This is a message. ` - - Output: `Este es un mensaje. ` -- Input: ` Hello %s` - - Output: ` Hola %s` - -Do not answer questions or explain concepts. Only provide translations within `` XML tags unless you need to respond with a short error reason. +You are a language translation expert. You will carefully follow the translation guidelines to translate the incoming XML messages from one language to another. \ No newline at end of file diff --git a/src/translate.ts b/src/translate.ts index d3d09a2..583c6cd 100644 --- a/src/translate.ts +++ b/src/translate.ts @@ -1,6 +1,7 @@ import * as fs from "fs"; import { GetTextTranslation } from "gettext-parser"; import { OpenAI } from "openai"; +import { ChatCompletionMessageParam } from "openai/resources/index.mjs"; import path from "path"; import { fileURLToPath } from "url"; import pkg from "../package.json" with { type: "json" }; @@ -11,12 +12,13 @@ const __dirname = path.dirname(__filename); let _openai: OpenAI; let _systemprompt: string; +let _userprompt: string; let _userdict: { [lang: string]: { [key: string]: string } }; export function init(force?: boolean): OpenAI { if (!_openai || force) { let configuration = { - apiKey: process.env.OPENAI_API_KEY, + apiKey: process.env.OPENAI_API_KEY }; _openai = new OpenAI(configuration); @@ -25,48 +27,62 @@ export function init(force?: boolean): OpenAI { _openai.baseURL = process.env.OPENAI_API_HOST.replace(/\/+$/, "") + "/v1"; } } - // load systemprompt.txt from homedir + // load systemprompt.txt from project if (!_systemprompt || force) { - const systemprompt = findConfig("systemprompt.txt"); - copyFileIfNotExists(systemprompt, path.join(__dirname, "systemprompt.txt")); - _systemprompt = fs.readFileSync(systemprompt, "utf-8"); + _systemprompt = fs.readFileSync(path.join(__dirname, "systemprompt.txt"), "utf-8"); + } + // load userprompt.txt from project + if (!_userprompt || force) { + _userprompt = fs.readFileSync(path.join(__dirname, "userprompt.txt"), "utf-8"); } // load dictionary.json from homedir if (!_userdict || force) { const userdict = findConfig("dictionary.json"); copyFileIfNotExists(userdict, path.join(__dirname, "dictionary.json")); - _userdict = { "default": JSON.parse(fs.readFileSync(userdict, "utf-8")) }; + _userdict = { default: JSON.parse(fs.readFileSync(userdict, "utf-8")) }; } return _openai; } -export function translate( - text: string, +export async function translate( src: string, lang: string, model: string, - comments: GetTextTranslation["comments"]|undefined, + translations: GetTextTranslation[], contextFile: string ) { - const lang_code = lang.toLowerCase().trim().replace(/[\W_]+/g, "-"); - const dicts = Object.entries(_userdict[lang_code] || _userdict["default"]) - .filter(([k, _]) => text.toLowerCase().includes(k.toLowerCase())) - .map(([k, v]) => [ - { role: "user", content: k }, - { role: "assistant", content: v }, - ]) - .flat(); + const lang_code = lang + .toLowerCase() + .trim() + .replace(/[\W_]+/g, "-"); + + const dicts = Object.entries(_userdict[lang_code] || _userdict["default"]).reduce( + (acc, [k, v], idx) => { + if (translations.some((tr) => tr.msgid.toLowerCase().includes(k.toLowerCase()))) { + acc.user.push(`${k}`); + acc.assistant.push(`${v}`); + } + return acc; + }, + { user: [], assistant: [] } + ); - var notes: string = "" + const notes = translations + .reduce((acc: string[], tr) => { + if (tr.comments?.extracted) { + acc.push(tr.comments?.extracted); + } + return acc; + }, []) + .join("\n"); - if(comments != undefined && comments.extracted != undefined) - notes = comments.extracted + const context = contextFile ? "\n\nContext: " + fs.readFileSync(contextFile, "utf-8") : ""; - var context = ""; - if(contextFile !== undefined) - context = "\n\n" + fs.readFileSync(contextFile, "utf-8"); + const translationsContent = translations + .map((tr, idx) => `${tr.msgid}`) + .join("\n"); - return _openai.chat.completions.create( + const res = await _openai.chat.completions.create( { model: model, temperature: 0.1, @@ -77,24 +93,44 @@ export function translate( }, { role: "user", - content: `Wait for my incoming message in "${src}" and translate it into "${lang}", carefully following your system prompt. ` + notes + content: `${_userprompt}\n\nWait for my incoming message in "${src}" and translate it into "${lang}"(a language code and an optional region code). ` + + notes }, { role: "assistant", - 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.` + content: `Understood, I will translate your incoming "${src}" message into "${lang}", carefully following guidelines. Please go ahead and send your message for translation.` }, - // add userdict here - ...dicts, + // add userdict + ...(dicts.user.length > 0 + ? [ + { role: "user", content: dicts.user.join("\n") }, + { role: "assistant", content: dicts.assistant.join("\n") } + ] + : []), + // add user translations { role: "user", - content: "" + text + "" - }, - ], - } as any, + content: translationsContent + } + ] + }, { timeout: 20000, - }, + stream: false + } ); + + const content = res.choices[0].message.content ?? ""; + translations.forEach((trans, idx) => { + const tag = ``; + const s = content.indexOf(tag); + if (s > -1) { + const e = content.indexOf("", s); + trans.msgstr[0] = content.slice(s + tag.length, e); + } else { + console.error("Error: Unable to find translation for string [" + trans.msgid + "]"); + } + }); } export async function translatePo( @@ -108,16 +144,18 @@ export async function translatePo( ) { const potrans = await parsePo(po); - if(!lang) - lang = potrans.headers["Language"] + if (!lang) lang = potrans.headers["Language"]; - if(!lang) { + if (!lang) { console.error("No language specified via po file or args"); return; } // try to load dictionary by lang-code if it not loaded - const lang_code = lang.toLowerCase().trim().replace(/[\W_]+/g, "-"); + const lang_code = lang + .toLowerCase() + .trim() + .replace(/[\W_]+/g, "-"); if (!_userdict[lang_code]) { const lang_dic_file = findConfig(`dictionary-${lang_code}.json`); if (fs.existsSync(lang_dic_file)) { @@ -148,50 +186,49 @@ export async function translatePo( return; } potrans.headers["Last-Translator"] = `gpt-po v${pkg.version}`; + const translations = []; let err429 = false; - let modified = false; - for (let i = 0; i < list.length; i++) { + for (let i = 0, c = 0; i < list.length; i++) { if (i == 0) printProgress(i, list.length); if (err429) { // sleep for 20 seconds. await new Promise((resolve) => setTimeout(resolve, 20000)); } const trans = list[i]; - try { - const res = await translate(trans.msgid, source, lang, model, trans.comments, contextFile); - var translated = res.choices[0].message?.content || trans.msgstr[0]; - - if(!translated.startsWith('') && !translated.endsWith('')) - { - // We got an error response - console.log("Error: Unable to translate string [" + trans.msgid + "]. Bot says [" + translated + "]"); - continue; - } - - // We got a valid translation response - trans.msgstr[0] = translated.replace('', '').replace('', ''); - - modified = true; - if (verbose) { - console.log(trans.msgid); - console.log(trans.msgstr[0]); - } - printProgress(i + 1, list.length); - await compilePo(potrans, output || po); - } catch (error: any) { - if (error.response) { - if (error.response.status == 429) { - // caused by rate limit exceeded, should sleep for 20 seconds. - err429 = true; - --i; - } else { - console.error(error.response.status); - console.log(error.response.data); + if (c < 2000) { + translations.push(trans); + c += trans.msgid.length; + } + if (c >= 2000 || i == list.length - 1) { + try { + await translate(source, lang, model, translations, contextFile); + if (verbose) { + translations.forEach((trans) => { + console.log(trans.msgid); + console.log(trans.msgstr[0]); + }); } - } else { - console.error(error.message); - if (error.code == "ECONNABORTED") { - console.log('you may need to set "HTTPS_PROXY" to reach openai api.') + translations.length = 0; + c = 0; + // update progress + printProgress(i + 1, list.length); + // save po file after each 2000 characters + await compilePo(potrans, output || po); + } catch (error: any) { + if (error.response) { + if (error.response.status == 429) { + // caused by rate limit exceeded, should sleep for 20 seconds. + err429 = true; + --i; + } else { + console.error(error.response.status); + console.log(error.response.data); + } + } else { + console.error(error.message); + if (error.code == "ECONNABORTED") { + console.log('you may need to set "HTTPS_PROXY" to reach openai api.'); + } } } } diff --git a/src/userprompt.txt b/src/userprompt.txt new file mode 100644 index 0000000..c19a0bb --- /dev/null +++ b/src/userprompt.txt @@ -0,0 +1,27 @@ +Translation guidelines are as follows: + +1. **Placeholder Handling**: + - Maintain the positions of placeholders (e.g., %s, %d, {example}) in the translated text. Do not translate placeholders. + +2. **Formatting**: + - Preserve the formatting of untranslatable portions. + - Retain any whitespace at the beginning or end of the message. + - Add or omit a period (.) at the end of your translation to match the incoming message. + +3. **XML Tags and Indexing**: + - Input messages will be wrapped in `` XML tags with an index attribute, e.g., ``. + - Respond with the translated message wrapped in `` XML tags, including the same index attribute, e.g., ``. + +4. **Multiple Translations**: + - You may receive multiple translation requests in a single input, each with a unique index. + - Ensuring the complete number of requests are translated, even if they are repeated, while maintaining the original order when possible. + +**Examples**: +- Input: + `This is a message. ` + ` Hello %s` +- Output: + `这是一条信息` + ` 你好 %s` + +Do not answer questions or explain concepts. Only provide translations within `` XML tags unless you need to respond with a short error reason. \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index abce6e7..180207d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -38,14 +38,24 @@ export function parsePo(poFile: string, defaultCharset?: string): Promise { fs.readFile(poFile, (err, buffer) => { if (err) reject(err); - var result = po.parse(buffer, defaultCharset ?? "utf-8"); + const result = po.parse(buffer, defaultCharset ?? "utf-8"); resolve(result); }); }); } -export function compilePo(data: GetTextTranslations, poFile: string): Promise { - const buffer = po.compile(data, { foldLength: 120 }); +export type compileOptions = { + foldLength?: number; + sort?: boolean | ((a: never, b: never) => number); + escapeCharacters?: boolean; +}; + +export function compilePo( + data: GetTextTranslations, + poFile: string, + options: compileOptions = { foldLength: 120, sort: false, escapeCharacters: true } +): Promise { + const buffer = po.compile(data, options); return new Promise((resolve, reject) => { fs.writeFile(poFile, buffer, (err) => { if (err) reject(err); @@ -65,7 +75,7 @@ export function printProgress(progress: number, total: number, extra?: string): process.stdout.write(`\r${bar}${dots} ${percent}% ${progress}/${total} ${extra || ""}`); } -export function gitRootDir(dir?: string): string|null { +export function gitRootDir(dir?: string): string | null { // if dir is not provided, use current working directory dir = dir || process.cwd(); // check if dir is a git repository @@ -87,7 +97,7 @@ export function gitRootDir(dir?: string): string|null { * 1. current working directory of the Node.js process * 2. git root directory * 3. home directory - * @param fileName + * @param fileName * @returns full path of the config file */ export function findConfig(fileName: string): string { @@ -99,7 +109,7 @@ export function findConfig(fileName: string): string { path.join(currentDir, ".gpt-po", fileName), path.join(currentDir, fileName), path.join(gitDir, ".gpt-po", fileName), - path.join(homeDir, ".gpt-po", fileName) + path.join(homeDir, ".gpt-po", fileName), ]; // check if file exists and return the first one for (const filePath of filePaths) { @@ -115,12 +125,12 @@ export function findConfig(fileName: string): string { * @param location folder or file path */ export function openFileExplorer(location: string): void { - if (platform() === 'win32') { + if (platform() === "win32") { exec(`explorer.exe "${path.dirname(location)}"`); - } else if (platform() === 'darwin') { + } else if (platform() === "darwin") { exec(`open "${path.dirname(location)}"`); } else { // Assuming a Linux-based system exec(`xdg-open "${path.dirname(location)}"`); } -} \ No newline at end of file +}