msgai-cli 1.1.0 → 1.1.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.
- package/CHANGELOG.md +6 -0
- package/README.md +9 -2
- package/dist/src/cli/index.js +23 -4
- package/dist/src/cli/runTranslate.js +68 -4
- package/dist/src/debug.js +36 -0
- package/dist/src/translate.js +63 -18
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [1.1.1](https://github.com/AlexMost/msgai/compare/v1.1.0...v1.1.1) (2026-03-01)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- normalize msgctxt validation and add debug mode ([7488a78](https://github.com/AlexMost/msgai/commit/7488a782594253c4cfc46b56500ca7fa7b102766))
|
|
10
|
+
|
|
5
11
|
## [1.1.0](https://github.com/AlexMost/msgai/compare/v1.0.2...v1.1.0) (2026-03-01)
|
|
6
12
|
|
|
7
13
|
### Features
|
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ msgai messages.po --api-key sk-...
|
|
|
79
79
|
Usage:
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
msgai <file.po> [--dry-run] [--api-key KEY] [--source-lang LANG] [--model MODEL] [--include-fuzzy]
|
|
82
|
+
msgai <file.po> [--dry-run] [--api-key KEY] [--source-lang LANG] [--model MODEL] [--include-fuzzy] [--debug]
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
Options:
|
|
@@ -89,11 +89,18 @@ Options:
|
|
|
89
89
|
- `--source-lang LANG`: set the source language of `msgid` strings as an ISO 639-1 code such as `en` or `uk`
|
|
90
90
|
- `--model MODEL`: set the OpenAI model used for translation; default is `gpt-4o`. Only models with `json_schema` structured outputs are supported.
|
|
91
91
|
- `--api-key KEY`: pass the OpenAI API key directly instead of using `OPENAI_API_KEY`
|
|
92
|
+
- `--debug`: print debug logs for batch preparation, OpenAI request retries, request payloads, and raw response validation
|
|
92
93
|
- `--help`: print command usage
|
|
93
94
|
|
|
95
|
+
You can also enable the same debug logging with the environment variable `DEBUG=1`:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
DEBUG=1 msgai messages.po
|
|
99
|
+
```
|
|
100
|
+
|
|
94
101
|
If no API key is provided for a non-dry run, the CLI exits with code `1` and prints an error message.
|
|
95
102
|
|
|
96
|
-
On API failures such as rate limits, quota issues, or server errors, the CLI exits with code `1` and shows a status-specific message. For API error details, see [OpenAI API error codes](https://developers.openai.com/api/docs/guides/error-codes#api-errors).
|
|
103
|
+
On API failures such as rate limits, quota issues, or server errors, the CLI exits with code `1` and shows a status-specific message. Validation errors for protected fields such as `msgid`, `msgid_plural`, or `msgctxt` now tell you whether a retry is reasonable and when to rerun with `--debug` or `DEBUG=1` to inspect the request/response flow. For API error details, see [OpenAI API error codes](https://developers.openai.com/api/docs/guides/error-codes#api-errors).
|
|
97
104
|
|
|
98
105
|
## 🧪 Development
|
|
99
106
|
|
package/dist/src/cli/index.js
CHANGED
|
@@ -6,12 +6,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const yargs_1 = __importDefault(require("yargs/yargs"));
|
|
8
8
|
const helpers_1 = require("yargs/helpers");
|
|
9
|
+
const debug_1 = require("../debug");
|
|
9
10
|
const runTranslate_1 = require("./runTranslate");
|
|
10
11
|
function parseArgs(argv) {
|
|
11
12
|
try {
|
|
12
13
|
const parsedArgs = (0, yargs_1.default)(argv)
|
|
13
14
|
.scriptName('msgai')
|
|
14
|
-
.usage('Usage: msgai <file.po> [--dry-run] [--api-key KEY] [--source-lang LANG] [--model MODEL] [--include-fuzzy]')
|
|
15
|
+
.usage('Usage: msgai <file.po> [--dry-run] [--api-key KEY] [--source-lang LANG] [--model MODEL] [--include-fuzzy] [--debug]')
|
|
15
16
|
.option('dry-run', {
|
|
16
17
|
type: 'boolean',
|
|
17
18
|
default: false,
|
|
@@ -37,6 +38,11 @@ function parseArgs(argv) {
|
|
|
37
38
|
alias: 'h',
|
|
38
39
|
type: 'boolean',
|
|
39
40
|
default: false,
|
|
41
|
+
})
|
|
42
|
+
.option('debug', {
|
|
43
|
+
type: 'boolean',
|
|
44
|
+
default: false,
|
|
45
|
+
description: 'Print debug logs for request/response validation and batch processing',
|
|
40
46
|
})
|
|
41
47
|
.strictOptions()
|
|
42
48
|
.version(false)
|
|
@@ -56,17 +62,19 @@ function parseArgs(argv) {
|
|
|
56
62
|
const modelRaw = parsedArgs.model;
|
|
57
63
|
const model = modelRaw != null && String(modelRaw).trim() !== '' ? String(modelRaw).trim() : undefined;
|
|
58
64
|
if (positionalArgs.length > 1) {
|
|
59
|
-
|
|
65
|
+
const result = {
|
|
60
66
|
dryRun: Boolean(parsedArgs['dry-run']),
|
|
61
67
|
help: Boolean(parsedArgs.help),
|
|
62
68
|
apiKey: parsedArgs['api-key'],
|
|
63
69
|
sourceLang,
|
|
64
70
|
model,
|
|
65
71
|
includeFuzzy: Boolean(parsedArgs['include-fuzzy']),
|
|
72
|
+
debug: Boolean(parsedArgs.debug),
|
|
66
73
|
error: `Unexpected argument: ${positionalArgs[1]}`,
|
|
67
74
|
};
|
|
75
|
+
return result;
|
|
68
76
|
}
|
|
69
|
-
|
|
77
|
+
const result = {
|
|
70
78
|
poFilePath: positionalArgs[0],
|
|
71
79
|
dryRun: Boolean(parsedArgs['dry-run']),
|
|
72
80
|
help: Boolean(parsedArgs.help),
|
|
@@ -74,25 +82,33 @@ function parseArgs(argv) {
|
|
|
74
82
|
sourceLang,
|
|
75
83
|
model,
|
|
76
84
|
includeFuzzy: Boolean(parsedArgs['include-fuzzy']),
|
|
85
|
+
debug: Boolean(parsedArgs.debug),
|
|
77
86
|
};
|
|
87
|
+
return result;
|
|
78
88
|
}
|
|
79
89
|
catch (error) {
|
|
80
90
|
const message = error instanceof Error ? error.message : String(error);
|
|
81
91
|
return { dryRun: false, help: false, error: message };
|
|
82
92
|
}
|
|
83
93
|
}
|
|
84
|
-
const USAGE = 'Usage: msgai <file.po> [--dry-run] [--api-key KEY] [--source-lang LANG] [--model MODEL] [--include-fuzzy]';
|
|
94
|
+
const USAGE = 'Usage: msgai <file.po> [--dry-run] [--api-key KEY] [--source-lang LANG] [--model MODEL] [--include-fuzzy] [--debug]';
|
|
85
95
|
function main(argv) {
|
|
86
96
|
const args = parseArgs(argv);
|
|
97
|
+
(0, debug_1.initDebugLogger)(args.debug);
|
|
98
|
+
const debugLogger = (0, debug_1.getDebugLogger)();
|
|
99
|
+
debugLogger.log('cli.main', 'Entering CLI main', { argv, args });
|
|
87
100
|
if (args.error) {
|
|
101
|
+
debugLogger.log('cli.main', 'Exiting because args contained an error', { error: args.error });
|
|
88
102
|
console.warn(args.error);
|
|
89
103
|
console.warn(USAGE);
|
|
90
104
|
return 1;
|
|
91
105
|
}
|
|
92
106
|
if (args.help) {
|
|
107
|
+
debugLogger.log('cli.main', 'Printing help output');
|
|
93
108
|
console.log(USAGE);
|
|
94
109
|
return 0;
|
|
95
110
|
}
|
|
111
|
+
debugLogger.log('cli.main', 'Dispatching runTranslateCommand');
|
|
96
112
|
const result = (0, runTranslate_1.runTranslateCommand)({
|
|
97
113
|
poFilePath: args.poFilePath,
|
|
98
114
|
dryRun: args.dryRun,
|
|
@@ -100,11 +116,14 @@ function main(argv) {
|
|
|
100
116
|
sourceLang: args.sourceLang,
|
|
101
117
|
model: args.model,
|
|
102
118
|
includeFuzzy: args.includeFuzzy,
|
|
119
|
+
debug: args.debug,
|
|
103
120
|
});
|
|
104
121
|
if (result instanceof Promise) {
|
|
122
|
+
debugLogger.log('cli.main', 'runTranslateCommand returned a promise');
|
|
105
123
|
result.then((code) => process.exit(code));
|
|
106
124
|
return undefined;
|
|
107
125
|
}
|
|
126
|
+
debugLogger.log('cli.main', 'runTranslateCommand returned synchronously', { exitCode: result });
|
|
108
127
|
return result;
|
|
109
128
|
}
|
|
110
129
|
const exitCode = main((0, helpers_1.hideBin)(process.argv));
|
|
@@ -7,6 +7,7 @@ exports.runTranslate = runTranslate;
|
|
|
7
7
|
exports.runTranslateCommand = runTranslateCommand;
|
|
8
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
9
|
const plural_forms_1 = require("plural-forms");
|
|
10
|
+
const debug_1 = require("../debug");
|
|
10
11
|
const po_1 = require("../po");
|
|
11
12
|
const translate_1 = require("../translate");
|
|
12
13
|
const validate_source_lang_1 = require("../validate-source-lang");
|
|
@@ -47,9 +48,21 @@ function getInvalidModelMessage(model) {
|
|
|
47
48
|
`Supported model families: ${translate_1.SUPPORTED_STRUCTURED_OUTPUT_MODELS.join(', ')}.`,
|
|
48
49
|
].join(' ');
|
|
49
50
|
}
|
|
50
|
-
async function runTranslate(poFilePath, apiKey, sourceLang, model, includeFuzzy) {
|
|
51
|
+
async function runTranslate(poFilePath, apiKey, sourceLang, model, includeFuzzy, debug) {
|
|
52
|
+
(0, debug_1.initDebugLogger)(debug);
|
|
53
|
+
const debugLogger = (0, debug_1.getDebugLogger)();
|
|
51
54
|
try {
|
|
55
|
+
debugLogger.log('cli.runTranslate', 'Starting translation run', {
|
|
56
|
+
poFilePath,
|
|
57
|
+
sourceLang,
|
|
58
|
+
model: model ?? 'gpt-4o',
|
|
59
|
+
includeFuzzy: includeFuzzy === true,
|
|
60
|
+
});
|
|
52
61
|
const poContent = node_fs_1.default.readFileSync(poFilePath, 'utf8');
|
|
62
|
+
debugLogger.log('cli.runTranslate', 'Read PO file', {
|
|
63
|
+
poFilePath,
|
|
64
|
+
bytes: Buffer.byteLength(poContent, 'utf8'),
|
|
65
|
+
});
|
|
53
66
|
const parsedPo = (0, po_1.parsePoContent)(poContent);
|
|
54
67
|
const { entries } = (0, po_1.getEntriesToTranslate)(parsedPo, { includeFuzzy });
|
|
55
68
|
if (entries.length === 0) {
|
|
@@ -68,13 +81,30 @@ async function runTranslate(poFilePath, apiKey, sourceLang, model, includeFuzzy)
|
|
|
68
81
|
// locale not in plural-forms; rely on formula only
|
|
69
82
|
}
|
|
70
83
|
}
|
|
71
|
-
const options = { apiKey, sourceLanguage: sourceLang, formula, pluralSamples, model };
|
|
84
|
+
const options = { apiKey, sourceLanguage: sourceLang, formula, pluralSamples, model, debug };
|
|
85
|
+
debugLogger.log('cli.runTranslate', 'Computed translation run inputs', {
|
|
86
|
+
targetLanguage,
|
|
87
|
+
formula,
|
|
88
|
+
pluralSamples,
|
|
89
|
+
entryCount: entries.length,
|
|
90
|
+
entries,
|
|
91
|
+
});
|
|
72
92
|
for (let i = 0; i < entries.length; i += TRANSLATE_BATCH_SIZE) {
|
|
73
93
|
const batch = entries.slice(i, i + TRANSLATE_BATCH_SIZE);
|
|
74
94
|
const batchNum = Math.floor(i / TRANSLATE_BATCH_SIZE) + 1;
|
|
75
95
|
const totalBatches = Math.ceil(entries.length / TRANSLATE_BATCH_SIZE);
|
|
96
|
+
debugLogger.log('cli.runTranslate', 'Preparing translation batch', {
|
|
97
|
+
batch: batchNum,
|
|
98
|
+
totalBatches,
|
|
99
|
+
batchSize: batch.length,
|
|
100
|
+
entries: batch,
|
|
101
|
+
});
|
|
76
102
|
console.log(`Translating batch ${batchNum}/${totalBatches} (${batch.length} phrase${batch.length === 1 ? '' : 's'})...`);
|
|
77
103
|
const batchResults = await (0, translate_1.translateStrings)(batch, targetLanguage, options);
|
|
104
|
+
debugLogger.log('cli.runTranslate', 'Received translation batch results', {
|
|
105
|
+
batch: batchNum,
|
|
106
|
+
results: batchResults,
|
|
107
|
+
});
|
|
78
108
|
for (const r of batchResults) {
|
|
79
109
|
if (typeof r.msgstr === 'string') {
|
|
80
110
|
console.log(` ${r.msgid} => ${r.msgstr}`);
|
|
@@ -88,10 +118,17 @@ async function runTranslate(poFilePath, apiKey, sourceLang, model, includeFuzzy)
|
|
|
88
118
|
(0, po_1.clearFuzzyFromEntries)(parsedPo, batchResults);
|
|
89
119
|
}
|
|
90
120
|
node_fs_1.default.writeFileSync(poFilePath, (0, po_1.compilePo)(parsedPo));
|
|
121
|
+
debugLogger.log('cli.runTranslate', 'Wrote translated batch back to PO file', {
|
|
122
|
+
batch: batchNum,
|
|
123
|
+
poFilePath,
|
|
124
|
+
});
|
|
91
125
|
}
|
|
92
126
|
return 0;
|
|
93
127
|
}
|
|
94
128
|
catch (error) {
|
|
129
|
+
debugLogger.log('cli.runTranslate', 'Translation run failed', {
|
|
130
|
+
error: error instanceof Error ? error.message : String(error),
|
|
131
|
+
});
|
|
95
132
|
const apiMessage = getApiErrorMessage(error);
|
|
96
133
|
if (apiMessage != null) {
|
|
97
134
|
console.warn(apiMessage);
|
|
@@ -102,8 +139,11 @@ async function runTranslate(poFilePath, apiKey, sourceLang, model, includeFuzzy)
|
|
|
102
139
|
return 1;
|
|
103
140
|
}
|
|
104
141
|
}
|
|
105
|
-
const USAGE = 'Usage: msgai <file.po> [--dry-run] [--api-key KEY] [--source-lang LANG] [--model MODEL] [--include-fuzzy]';
|
|
142
|
+
const USAGE = 'Usage: msgai <file.po> [--dry-run] [--api-key KEY] [--source-lang LANG] [--model MODEL] [--include-fuzzy] [--debug]';
|
|
106
143
|
function runTranslateCommand(args) {
|
|
144
|
+
(0, debug_1.initDebugLogger)(args.debug);
|
|
145
|
+
const debugLogger = (0, debug_1.getDebugLogger)();
|
|
146
|
+
debugLogger.log('cli.runTranslateCommand', 'Received command args', args);
|
|
107
147
|
if (!args.poFilePath) {
|
|
108
148
|
console.warn(USAGE);
|
|
109
149
|
return 1;
|
|
@@ -113,6 +153,10 @@ function runTranslateCommand(args) {
|
|
|
113
153
|
(0, validate_source_lang_1.validateSourceLang)(args.sourceLang);
|
|
114
154
|
}
|
|
115
155
|
catch (error) {
|
|
156
|
+
debugLogger.log('cli.runTranslateCommand', 'Source language validation failed', {
|
|
157
|
+
sourceLang: args.sourceLang,
|
|
158
|
+
error: error instanceof Error ? error.message : String(error),
|
|
159
|
+
});
|
|
116
160
|
const message = error instanceof Error ? error.message : String(error);
|
|
117
161
|
console.warn(message);
|
|
118
162
|
return 1;
|
|
@@ -123,6 +167,9 @@ function runTranslateCommand(args) {
|
|
|
123
167
|
(0, translate_1.validateModel)(args.model);
|
|
124
168
|
}
|
|
125
169
|
catch {
|
|
170
|
+
debugLogger.log('cli.runTranslateCommand', 'Model validation failed', {
|
|
171
|
+
model: args.model,
|
|
172
|
+
});
|
|
126
173
|
console.warn(getInvalidModelMessage(args.model));
|
|
127
174
|
return 1;
|
|
128
175
|
}
|
|
@@ -131,20 +178,34 @@ function runTranslateCommand(args) {
|
|
|
131
178
|
let resultApiKey;
|
|
132
179
|
try {
|
|
133
180
|
resultApiKey = (0, translate_1.resolveApiKey)(args.apiKey);
|
|
181
|
+
debugLogger.log('cli.runTranslateCommand', 'Resolved API key for translation run', {
|
|
182
|
+
source: args.apiKey != null && args.apiKey.trim() !== '' ? 'cli-arg' : 'env',
|
|
183
|
+
});
|
|
134
184
|
}
|
|
135
185
|
catch (error) {
|
|
186
|
+
debugLogger.log('cli.runTranslateCommand', 'API key resolution failed', {
|
|
187
|
+
error: error instanceof Error ? error.message : String(error),
|
|
188
|
+
});
|
|
136
189
|
const message = error instanceof Error ? error.message : String(error);
|
|
137
190
|
console.warn(message.replace('pass apiKey in options', 'pass --api-key'));
|
|
138
191
|
return 1;
|
|
139
192
|
}
|
|
140
|
-
return runTranslate(args.poFilePath, resultApiKey, args.sourceLang, args.model, args.includeFuzzy);
|
|
193
|
+
return runTranslate(args.poFilePath, resultApiKey, args.sourceLang, args.model, args.includeFuzzy, args.debug);
|
|
141
194
|
}
|
|
142
195
|
try {
|
|
143
196
|
const poContent = node_fs_1.default.readFileSync(args.poFilePath, 'utf8');
|
|
197
|
+
debugLogger.log('cli.runTranslateCommand', 'Dry-run read PO file', {
|
|
198
|
+
poFilePath: args.poFilePath,
|
|
199
|
+
bytes: Buffer.byteLength(poContent, 'utf8'),
|
|
200
|
+
});
|
|
144
201
|
const parsedPo = (0, po_1.parsePoContent)(poContent);
|
|
145
202
|
const { entries } = (0, po_1.getEntriesToTranslate)(parsedPo, {
|
|
146
203
|
includeFuzzy: args.includeFuzzy,
|
|
147
204
|
});
|
|
205
|
+
debugLogger.log('cli.runTranslateCommand', 'Dry-run extracted entries', {
|
|
206
|
+
entryCount: entries.length,
|
|
207
|
+
entries,
|
|
208
|
+
});
|
|
148
209
|
const msgidsToShow = entries.map((e) => e.msgid);
|
|
149
210
|
for (const msgid of msgidsToShow) {
|
|
150
211
|
console.log(msgid);
|
|
@@ -152,6 +213,9 @@ function runTranslateCommand(args) {
|
|
|
152
213
|
return 0;
|
|
153
214
|
}
|
|
154
215
|
catch (error) {
|
|
216
|
+
debugLogger.log('cli.runTranslateCommand', 'Dry-run failed', {
|
|
217
|
+
error: error instanceof Error ? error.message : String(error),
|
|
218
|
+
});
|
|
155
219
|
const message = error instanceof Error ? error.message : String(error);
|
|
156
220
|
console.warn(`Failed to process PO file: ${message}`);
|
|
157
221
|
return 1;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasDebugFlag = hasDebugFlag;
|
|
4
|
+
exports.initDebugLogger = initDebugLogger;
|
|
5
|
+
exports.getDebugLogger = getDebugLogger;
|
|
6
|
+
const debugState = {
|
|
7
|
+
enabled: false,
|
|
8
|
+
};
|
|
9
|
+
const debugLogger = {
|
|
10
|
+
get enabled() {
|
|
11
|
+
return debugState.enabled;
|
|
12
|
+
},
|
|
13
|
+
log(scope, message, details) {
|
|
14
|
+
if (!debugState.enabled)
|
|
15
|
+
return;
|
|
16
|
+
const prefix = `[debug] [${scope}] ${message}`;
|
|
17
|
+
if (details === undefined) {
|
|
18
|
+
console.warn(prefix);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.warn(prefix, details);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
function hasDebugFlag(argv) {
|
|
25
|
+
return argv.includes('--debug');
|
|
26
|
+
}
|
|
27
|
+
function isDebugEnvEnabled() {
|
|
28
|
+
return process.env['DEBUG'] === '1';
|
|
29
|
+
}
|
|
30
|
+
function initDebugLogger(enabled) {
|
|
31
|
+
debugState.enabled = enabled === true || isDebugEnvEnabled();
|
|
32
|
+
return debugLogger;
|
|
33
|
+
}
|
|
34
|
+
function getDebugLogger() {
|
|
35
|
+
return debugLogger;
|
|
36
|
+
}
|
package/dist/src/translate.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.translatePayload = translatePayload;
|
|
|
10
10
|
exports.translateItems = translateItems;
|
|
11
11
|
exports.translateStrings = translateStrings;
|
|
12
12
|
const openai_1 = __importDefault(require("openai"));
|
|
13
|
+
const debug_1 = require("./debug");
|
|
13
14
|
const loadEnv_1 = require("./loadEnv");
|
|
14
15
|
function resolveApiKey(apiKey) {
|
|
15
16
|
(0, loadEnv_1.loadEnv)();
|
|
@@ -211,11 +212,26 @@ function stripJsonFences(raw) {
|
|
|
211
212
|
const match = trimmed.match(jsonBlock);
|
|
212
213
|
return match ? match[1].trim() : trimmed;
|
|
213
214
|
}
|
|
214
|
-
function
|
|
215
|
+
function normalizeMsgctxt(msgctxt) {
|
|
216
|
+
return typeof msgctxt === 'string' ? msgctxt : '';
|
|
217
|
+
}
|
|
218
|
+
function buildProtectedFieldMismatchMessage(index, field) {
|
|
219
|
+
const entryRef = `OpenAI response translations[${index}].${field}`;
|
|
220
|
+
const retryHint = 'Retry the command once because this can be a transient structured-output formatting issue.';
|
|
221
|
+
const debugHint = 'If it keeps happening, rerun with --debug and double-check that the PO entry content matches the returned protected fields.';
|
|
222
|
+
if (field === 'msgctxt') {
|
|
223
|
+
return `${entryRef} must match the input exactly. ${retryHint} If it keeps happening, rerun with --debug and check whether empty gettext context is being returned as omitted vs empty string.`;
|
|
224
|
+
}
|
|
225
|
+
return `${entryRef} must match the input exactly. ${retryHint} ${debugHint}`;
|
|
226
|
+
}
|
|
227
|
+
function parsePayloadResponse(request, content, options) {
|
|
228
|
+
(0, debug_1.initDebugLogger)(options?.debug);
|
|
229
|
+
const debug = (0, debug_1.getDebugLogger)();
|
|
215
230
|
if (content == null || content.trim() === '') {
|
|
216
231
|
throw new Error('Empty response from OpenAI');
|
|
217
232
|
}
|
|
218
233
|
const raw = content.trim();
|
|
234
|
+
debug.log('translate', 'Raw OpenAI response content received', raw);
|
|
219
235
|
const toParse = stripJsonFences(raw);
|
|
220
236
|
let parsed;
|
|
221
237
|
try {
|
|
@@ -246,13 +262,14 @@ function parsePayloadResponse(request, content) {
|
|
|
246
262
|
const entry = t;
|
|
247
263
|
const msgstr = entry.msgstr;
|
|
248
264
|
const requestEntry = request.translations[i];
|
|
249
|
-
const requestContext = requestEntry.msgctxt;
|
|
250
|
-
|
|
251
|
-
|
|
265
|
+
const requestContext = normalizeMsgctxt(requestEntry.msgctxt);
|
|
266
|
+
const responseContext = normalizeMsgctxt(entry.msgctxt);
|
|
267
|
+
if (responseContext !== requestContext) {
|
|
268
|
+
throw new Error(buildProtectedFieldMismatchMessage(i, 'msgctxt'));
|
|
252
269
|
}
|
|
253
270
|
if ('msgid' in requestEntry) {
|
|
254
271
|
if (entry.msgid !== requestEntry.msgid) {
|
|
255
|
-
throw new Error(
|
|
272
|
+
throw new Error(buildProtectedFieldMismatchMessage(i, 'msgid'));
|
|
256
273
|
}
|
|
257
274
|
if ('msgid_plural' in entry) {
|
|
258
275
|
throw new Error(`OpenAI response translations[${i}] must not include msgid_plural`);
|
|
@@ -263,7 +280,7 @@ function parsePayloadResponse(request, content) {
|
|
|
263
280
|
throw new Error(`OpenAI response translations[${i}].msgstr must be a string`);
|
|
264
281
|
}
|
|
265
282
|
if (entry.msgid_plural !== requestEntry.msgid_plural) {
|
|
266
|
-
throw new Error(
|
|
283
|
+
throw new Error(buildProtectedFieldMismatchMessage(i, 'msgid_plural'));
|
|
267
284
|
}
|
|
268
285
|
if ('msgid' in entry) {
|
|
269
286
|
throw new Error(`OpenAI response translations[${i}] must not include msgid`);
|
|
@@ -283,33 +300,61 @@ async function translatePayload(payload, options) {
|
|
|
283
300
|
if (payload.translations.length === 0) {
|
|
284
301
|
return { ...payload, translations: [] };
|
|
285
302
|
}
|
|
303
|
+
(0, debug_1.initDebugLogger)(options?.debug);
|
|
304
|
+
const debug = (0, debug_1.getDebugLogger)();
|
|
286
305
|
const client = options?.client ??
|
|
287
306
|
new openai_1.default({
|
|
288
307
|
apiKey: options.apiKey,
|
|
289
308
|
});
|
|
290
309
|
const model = options?.model ?? DEFAULT_MODEL;
|
|
291
310
|
validateStructuredOutputModel(model);
|
|
311
|
+
debug.log('translate', 'Prepared translatePayload request summary', {
|
|
312
|
+
model,
|
|
313
|
+
target_language: payload.target_language,
|
|
314
|
+
source_language: payload.source_language,
|
|
315
|
+
translation_count: payload.translations.length,
|
|
316
|
+
plural_samples: payload.plural_samples?.length ?? 0,
|
|
317
|
+
});
|
|
318
|
+
debug.log('translate', 'translatePayload request payload', payload);
|
|
319
|
+
const requestParams = {
|
|
320
|
+
model,
|
|
321
|
+
temperature: 0,
|
|
322
|
+
response_format: {
|
|
323
|
+
type: 'json_schema',
|
|
324
|
+
json_schema: TRANSLATION_RESPONSE_SCHEMA,
|
|
325
|
+
},
|
|
326
|
+
messages: [
|
|
327
|
+
{ role: 'system', content: buildSystemMessage() },
|
|
328
|
+
{ role: 'user', content: JSON.stringify(payload) },
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
debug.log('translate', 'OpenAI chat.completions.create request', requestParams);
|
|
292
332
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
293
333
|
try {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
],
|
|
334
|
+
debug.log('translate', 'Sending request to OpenAI', {
|
|
335
|
+
attempt: attempt + 1,
|
|
336
|
+
max_attempts: MAX_RETRIES + 1,
|
|
337
|
+
});
|
|
338
|
+
const response = await client.chat.completions.create(requestParams);
|
|
339
|
+
debug.log('translate', 'OpenAI chat.completions.create response metadata', {
|
|
340
|
+
id: response.id,
|
|
341
|
+
model: response.model,
|
|
342
|
+
finish_reason: response.choices[0]?.finish_reason ?? null,
|
|
343
|
+
choices: response.choices.length,
|
|
305
344
|
});
|
|
306
345
|
const content = response.choices[0]?.message?.content ?? null;
|
|
307
|
-
return parsePayloadResponse(payload, content);
|
|
346
|
+
return parsePayloadResponse(payload, content, { debug: options?.debug });
|
|
308
347
|
}
|
|
309
348
|
catch (err) {
|
|
310
349
|
const shouldRetry = attempt < MAX_RETRIES && isApiError(err) && isRetryableStatus(err.status);
|
|
350
|
+
debug.log('translate', 'translatePayload request failed', {
|
|
351
|
+
attempt: attempt + 1,
|
|
352
|
+
shouldRetry,
|
|
353
|
+
error: err instanceof Error ? err.message : String(err),
|
|
354
|
+
});
|
|
311
355
|
if (shouldRetry) {
|
|
312
356
|
const delayMs = RETRY_DELAYS_MS[attempt] ?? RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1];
|
|
357
|
+
debug.log('translate', 'Retrying after backoff', { delay_ms: delayMs });
|
|
313
358
|
await sleep(delayMs);
|
|
314
359
|
continue;
|
|
315
360
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "msgai-cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "CLI that automatically translates all untranslated strings in gettext (.po) files using AI (LLM)",
|
|
5
5
|
"main": "dist/src/cli/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"test:integration": "npm run build && jest -c jest.integration.config.cjs",
|
|
14
14
|
"test:watch": "npm run build && jest --watch",
|
|
15
15
|
"format": "prettier --write .",
|
|
16
|
+
"format:changelog": "prettier --write CHANGELOG.md",
|
|
16
17
|
"lint": "eslint .",
|
|
17
18
|
"lint:format": "prettier --check .",
|
|
18
19
|
"prerelease": "npm run build && npm test && npm run test:integration && npm run lint && npm run lint:format",
|
|
@@ -51,7 +52,10 @@
|
|
|
51
52
|
"homepage": "https://github.com/AlexMost/msgai#readme",
|
|
52
53
|
"commit-and-tag-version": {
|
|
53
54
|
"tagPrefix": "v",
|
|
54
|
-
"releaseCommitMessageFormat": "chore(release): {{currentTag}}"
|
|
55
|
+
"releaseCommitMessageFormat": "chore(release): {{currentTag}}",
|
|
56
|
+
"scripts": {
|
|
57
|
+
"postchangelog": "npm run format:changelog"
|
|
58
|
+
}
|
|
55
59
|
},
|
|
56
60
|
"devDependencies": {
|
|
57
61
|
"@babel/core": "^7.29.0",
|