msgai-cli 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
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.2.0](https://github.com/AlexMost/msgai/compare/v1.1.1...v1.2.0) (2026-03-02)
6
+
7
+ ### Features
8
+
9
+ - add configurable po fold length ([343775c](https://github.com/AlexMost/msgai/commit/343775c8046abf6705ca02d687305d1db1e2c9c6))
10
+
11
+ ## [1.1.1](https://github.com/AlexMost/msgai/compare/v1.1.0...v1.1.1) (2026-03-01)
12
+
13
+ ### Bug Fixes
14
+
15
+ - normalize msgctxt validation and add debug mode ([7488a78](https://github.com/AlexMost/msgai/commit/7488a782594253c4cfc46b56500ca7fa7b102766))
16
+
5
17
  ## [1.1.0](https://github.com/AlexMost/msgai/compare/v1.0.2...v1.1.0) (2026-03-01)
6
18
 
7
19
  ### 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] [--fold-length N] [--debug]
83
83
  ```
84
84
 
85
85
  Options:
@@ -89,11 +89,19 @@ 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
+ - `--fold-length N`: set PO line fold length when writing files. Use `0` to disable folding and minimize formatting-only diffs. Default: `0`
93
+ - `--debug`: print debug logs for batch preparation, OpenAI request retries, request payloads, and raw response validation
92
94
  - `--help`: print command usage
93
95
 
96
+ You can also enable the same debug logging with the environment variable `DEBUG=1`:
97
+
98
+ ```bash
99
+ DEBUG=1 msgai messages.po
100
+ ```
101
+
94
102
  If no API key is provided for a non-dry run, the CLI exits with code `1` and prints an error message.
95
103
 
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).
104
+ 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
105
 
98
106
  ## 🧪 Development
99
107
 
@@ -6,12 +6,25 @@ 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");
11
+ function normalizeFoldLength(value) {
12
+ if (value == null) {
13
+ return undefined;
14
+ }
15
+ if (typeof value !== 'number' || !Number.isFinite(value) || !Number.isInteger(value)) {
16
+ throw new Error('Invalid --fold-length. Expected a non-negative integer.');
17
+ }
18
+ if (value < 0) {
19
+ throw new Error('Invalid --fold-length. Expected a non-negative integer.');
20
+ }
21
+ return value;
22
+ }
10
23
  function parseArgs(argv) {
11
24
  try {
12
25
  const parsedArgs = (0, yargs_1.default)(argv)
13
26
  .scriptName('msgai')
14
- .usage('Usage: msgai <file.po> [--dry-run] [--api-key KEY] [--source-lang LANG] [--model MODEL] [--include-fuzzy]')
27
+ .usage('Usage: msgai <file.po> [--dry-run] [--api-key KEY] [--source-lang LANG] [--model MODEL] [--include-fuzzy] [--fold-length N] [--debug]')
15
28
  .option('dry-run', {
16
29
  type: 'boolean',
17
30
  default: false,
@@ -32,11 +45,20 @@ function parseArgs(argv) {
32
45
  .option('model', {
33
46
  type: 'string',
34
47
  description: 'OpenAI model to use for translation. Default: gpt-4o',
48
+ })
49
+ .option('fold-length', {
50
+ type: 'number',
51
+ description: 'PO line fold length when writing files. Use 0 to disable folding. Default: 0',
35
52
  })
36
53
  .option('help', {
37
54
  alias: 'h',
38
55
  type: 'boolean',
39
56
  default: false,
57
+ })
58
+ .option('debug', {
59
+ type: 'boolean',
60
+ default: false,
61
+ description: 'Print debug logs for request/response validation and batch processing',
40
62
  })
41
63
  .strictOptions()
42
64
  .version(false)
@@ -55,18 +77,22 @@ function parseArgs(argv) {
55
77
  : undefined;
56
78
  const modelRaw = parsedArgs.model;
57
79
  const model = modelRaw != null && String(modelRaw).trim() !== '' ? String(modelRaw).trim() : undefined;
80
+ const foldLength = normalizeFoldLength(parsedArgs['fold-length']);
58
81
  if (positionalArgs.length > 1) {
59
- return {
82
+ const result = {
60
83
  dryRun: Boolean(parsedArgs['dry-run']),
61
84
  help: Boolean(parsedArgs.help),
62
85
  apiKey: parsedArgs['api-key'],
63
86
  sourceLang,
64
87
  model,
65
88
  includeFuzzy: Boolean(parsedArgs['include-fuzzy']),
89
+ foldLength,
90
+ debug: Boolean(parsedArgs.debug),
66
91
  error: `Unexpected argument: ${positionalArgs[1]}`,
67
92
  };
93
+ return result;
68
94
  }
69
- return {
95
+ const result = {
70
96
  poFilePath: positionalArgs[0],
71
97
  dryRun: Boolean(parsedArgs['dry-run']),
72
98
  help: Boolean(parsedArgs.help),
@@ -74,25 +100,34 @@ function parseArgs(argv) {
74
100
  sourceLang,
75
101
  model,
76
102
  includeFuzzy: Boolean(parsedArgs['include-fuzzy']),
103
+ foldLength,
104
+ debug: Boolean(parsedArgs.debug),
77
105
  };
106
+ return result;
78
107
  }
79
108
  catch (error) {
80
109
  const message = error instanceof Error ? error.message : String(error);
81
110
  return { dryRun: false, help: false, error: message };
82
111
  }
83
112
  }
84
- const USAGE = 'Usage: msgai <file.po> [--dry-run] [--api-key KEY] [--source-lang LANG] [--model MODEL] [--include-fuzzy]';
113
+ const USAGE = 'Usage: msgai <file.po> [--dry-run] [--api-key KEY] [--source-lang LANG] [--model MODEL] [--include-fuzzy] [--fold-length N] [--debug]';
85
114
  function main(argv) {
86
115
  const args = parseArgs(argv);
116
+ (0, debug_1.initDebugLogger)(args.debug);
117
+ const debugLogger = (0, debug_1.getDebugLogger)();
118
+ debugLogger.log('cli.main', 'Entering CLI main', { argv, args });
87
119
  if (args.error) {
120
+ debugLogger.log('cli.main', 'Exiting because args contained an error', { error: args.error });
88
121
  console.warn(args.error);
89
122
  console.warn(USAGE);
90
123
  return 1;
91
124
  }
92
125
  if (args.help) {
126
+ debugLogger.log('cli.main', 'Printing help output');
93
127
  console.log(USAGE);
94
128
  return 0;
95
129
  }
130
+ debugLogger.log('cli.main', 'Dispatching runTranslateCommand');
96
131
  const result = (0, runTranslate_1.runTranslateCommand)({
97
132
  poFilePath: args.poFilePath,
98
133
  dryRun: args.dryRun,
@@ -100,11 +135,15 @@ function main(argv) {
100
135
  sourceLang: args.sourceLang,
101
136
  model: args.model,
102
137
  includeFuzzy: args.includeFuzzy,
138
+ foldLength: args.foldLength,
139
+ debug: args.debug,
103
140
  });
104
141
  if (result instanceof Promise) {
142
+ debugLogger.log('cli.main', 'runTranslateCommand returned a promise');
105
143
  result.then((code) => process.exit(code));
106
144
  return undefined;
107
145
  }
146
+ debugLogger.log('cli.main', 'runTranslateCommand returned synchronously', { exitCode: result });
108
147
  return result;
109
148
  }
110
149
  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, foldLength, 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}`);
@@ -87,11 +117,18 @@ async function runTranslate(poFilePath, apiKey, sourceLang, model, includeFuzzy)
87
117
  if (includeFuzzy) {
88
118
  (0, po_1.clearFuzzyFromEntries)(parsedPo, batchResults);
89
119
  }
90
- node_fs_1.default.writeFileSync(poFilePath, (0, po_1.compilePo)(parsedPo));
120
+ node_fs_1.default.writeFileSync(poFilePath, (0, po_1.compilePo)(parsedPo, { foldLength }));
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] [--fold-length N] [--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.foldLength, 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/po.js CHANGED
@@ -10,6 +10,7 @@ exports.applyTranslations = applyTranslations;
10
10
  exports.clearFuzzyFromEntries = clearFuzzyFromEntries;
11
11
  exports.compilePo = compilePo;
12
12
  const gettext_parser_1 = require("gettext-parser");
13
+ const DEFAULT_FOLD_LENGTH = 0;
13
14
  function getUntranslatedMsgids(parsedPo) {
14
15
  const untranslatedMsgids = [];
15
16
  const translations = parsedPo.translations;
@@ -154,6 +155,9 @@ function clearFuzzyFromEntries(parsedPo, results) {
154
155
  /**
155
156
  * Compiles the parsed PO to a buffer (no file I/O).
156
157
  */
157
- function compilePo(parsedPo) {
158
- return gettext_parser_1.po.compile(parsedPo);
158
+ function compilePo(parsedPo, options) {
159
+ return gettext_parser_1.po.compile(parsedPo, {
160
+ foldLength: options?.foldLength ?? DEFAULT_FOLD_LENGTH,
161
+ sort: false,
162
+ });
159
163
  }
@@ -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 parsePayloadResponse(request, content) {
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
- if (entry.msgctxt !== requestContext) {
251
- throw new Error(`OpenAI response translations[${i}].msgctxt must match the input exactly`);
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(`OpenAI response translations[${i}].msgid must match the input exactly`);
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(`OpenAI response translations[${i}].msgid_plural must match the input exactly`);
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
- const response = await client.chat.completions.create({
295
- model,
296
- temperature: 0,
297
- response_format: {
298
- type: 'json_schema',
299
- json_schema: TRANSLATION_RESPONSE_SCHEMA,
300
- },
301
- messages: [
302
- { role: 'system', content: buildSystemMessage() },
303
- { role: 'user', content: JSON.stringify(payload) },
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.0",
3
+ "version": "1.2.0",
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",