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 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
 
@@ -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
- return {
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
- return {
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
+ }
@@ -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.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",