champollion 0.3.4 → 0.5.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.
Files changed (138) hide show
  1. package/README.md +41 -26
  2. package/bin/cli.js +53 -5
  3. package/index.js +63 -2
  4. package/lib/api-key.js +17 -4
  5. package/lib/autofix.js +82 -37
  6. package/lib/bridge/method_bridge.py +15 -3
  7. package/lib/cards/reader.js +34 -0
  8. package/lib/cards/remote.js +15 -0
  9. package/lib/cards/search-names.js +178 -0
  10. package/lib/command-help.js +286 -85
  11. package/lib/commands/audit.js +10 -3
  12. package/lib/commands/card.js +583 -226
  13. package/lib/commands/doctor.js +54 -18
  14. package/lib/commands/help.js +37 -32
  15. package/lib/commands/init.js +1709 -101
  16. package/lib/commands/integrity.js +127 -40
  17. package/lib/commands/leaderboard.js +187 -67
  18. package/lib/commands/models.js +9 -2
  19. package/lib/commands/provenance.js +7 -2
  20. package/lib/commands/recommend.js +43 -14
  21. package/lib/commands/register-corpus.js +632 -125
  22. package/lib/commands/seal-corpus.js +1 -1
  23. package/lib/commands/status.js +564 -27
  24. package/lib/commands/submit.js +17 -12
  25. package/lib/commands/sync.js +31 -7
  26. package/lib/commands/tm.js +15 -9
  27. package/lib/commands/verify.js +27 -3
  28. package/lib/commands/wrap.js +63 -5
  29. package/lib/commands/xliff.js +135 -64
  30. package/lib/commercial-eligibility.js +1 -1
  31. package/lib/config.js +230 -17
  32. package/lib/content-estimate.js +96 -0
  33. package/lib/content-refusals.js +284 -0
  34. package/lib/content-review.js +372 -0
  35. package/lib/content-sync.js +1136 -345
  36. package/lib/content.js +94 -7
  37. package/lib/corpus-registration.mjs +194 -35
  38. package/lib/cost-label.js +29 -0
  39. package/lib/cost-report.js +727 -79
  40. package/lib/diff.js +38 -4
  41. package/lib/docusaurus-sync.js +975 -262
  42. package/lib/edit-distance.js +31 -0
  43. package/lib/fallback.js +1102 -0
  44. package/lib/file-scope.js +106 -0
  45. package/lib/flatten.js +80 -3
  46. package/lib/flutter-locales.js +124 -0
  47. package/lib/format.js +266 -12
  48. package/lib/hash.js +146 -21
  49. package/lib/icu-structure.js +929 -0
  50. package/lib/integrity.js +223 -75
  51. package/lib/language-pair.js +157 -0
  52. package/lib/lint.js +78 -16
  53. package/lib/local-only-marks.js +106 -0
  54. package/lib/locale-layout.js +1103 -0
  55. package/lib/locale-state.js +575 -0
  56. package/lib/methods/anthropic.js +5 -0
  57. package/lib/methods/apertium.js +6 -3
  58. package/lib/methods/api.js +138 -25
  59. package/lib/methods/base.js +17 -0
  60. package/lib/methods/coaching-data.js +153 -0
  61. package/lib/methods/content-separator.js +43 -0
  62. package/lib/methods/deepl.js +1 -1
  63. package/lib/methods/direct-llm.js +257 -103
  64. package/lib/methods/external.js +146 -63
  65. package/lib/methods/gemini.js +1 -0
  66. package/lib/methods/google-translate.js +1 -0
  67. package/lib/methods/http-utils.js +41 -0
  68. package/lib/methods/libretranslate.js +7 -2
  69. package/lib/methods/llm-coached.js +71 -129
  70. package/lib/methods/llm.js +87 -33
  71. package/lib/methods/local.js +93 -10
  72. package/lib/methods/microsoft-translator.js +1 -2
  73. package/lib/methods/openai.js +4 -2
  74. package/lib/methods/openrouter-client.js +20 -19
  75. package/lib/methods/openrouter-pricing.js +150 -13
  76. package/lib/methods/prompt-methods.js +20 -0
  77. package/lib/methods/provider-pricing.js +42 -1
  78. package/lib/methods/request-capture.js +104 -0
  79. package/lib/methods/tilde.js +1 -1
  80. package/lib/methods/translated.js +1 -2
  81. package/lib/missing-key.js +93 -0
  82. package/lib/models.js +102 -55
  83. package/lib/name-rules.js +32 -0
  84. package/lib/named-keys.js +172 -0
  85. package/lib/no-translate.js +4 -3
  86. package/lib/output.js +160 -19
  87. package/lib/pairs.js +586 -30
  88. package/lib/placeholders.js +394 -0
  89. package/lib/plugins.js +8 -0
  90. package/lib/plural-gap-redo.js +109 -0
  91. package/lib/plurals.js +323 -0
  92. package/lib/po.js +1187 -0
  93. package/lib/public-catalogue.js +74 -0
  94. package/lib/recommend.js +527 -32
  95. package/lib/redo.js +95 -0
  96. package/lib/refusal-category.js +44 -0
  97. package/lib/registers.js +255 -11
  98. package/lib/repair-script.js +20 -13
  99. package/lib/scripts.js +6 -1
  100. package/lib/seal.mjs +4 -3
  101. package/lib/sealed-qualifier.mjs +1 -1
  102. package/lib/segment.js +24 -8
  103. package/lib/seo.js +19 -9
  104. package/lib/serve.js +43 -6
  105. package/lib/shared-output-seed.js +164 -0
  106. package/lib/source-contexts.js +39 -0
  107. package/lib/submit.mjs +57 -5
  108. package/lib/sync.js +2923 -474
  109. package/lib/terminology.js +13 -4
  110. package/lib/tm-evict.js +179 -0
  111. package/lib/tm-seed.js +5 -2
  112. package/lib/tm.js +818 -36
  113. package/lib/translate-pair.js +639 -34
  114. package/lib/translate.js +78 -5
  115. package/lib/types.js +22 -3
  116. package/lib/validate.js +996 -21
  117. package/lib/verify.js +1332 -104
  118. package/lib/watch.js +32 -13
  119. package/lib/xliff.js +44 -3
  120. package/package.json +1 -1
  121. package/shared/CORPORA-CARDS.md +2 -0
  122. package/shared/cards-fallback.json +1 -1
  123. package/shared/curated-orthography-conventions.json +26 -8
  124. package/shared/gettext-plural-forms.json +45 -0
  125. package/shared/licenses.json +99 -3
  126. package/shared/method-registry.json +2 -0
  127. package/shared/metric-registry.json +96 -18
  128. package/shared/retired-model-aliases.json +9 -0
  129. package/shared/schemas/champollion-plugin.schema.json +4 -0
  130. package/shared/schemas/corpora-card.schema.json +8 -2
  131. package/shared/schemas/human-services.schema.json +1 -1
  132. package/shared/schemas/method-index-record.schema.json +67 -0
  133. package/shared/schemas/method-registry.schema.json +4 -0
  134. package/shared/schemas/metric-registry.schema.json +55 -1
  135. package/shared/schemas/retired-model-aliases.schema.json +32 -0
  136. package/shared/docent/corpus.json +0 -11739
  137. package/shared/model-aliases.json +0 -7
  138. package/shared/schemas/model-aliases.schema.json +0 -27
package/lib/output.js CHANGED
@@ -31,6 +31,44 @@
31
31
  */
32
32
  let mode = 'default';
33
33
 
34
+ /**
35
+ * Keys as people can copy them. A gettext key with a context is
36
+ * `msgctxt + U+0004 + msgid` — gettext's own encoding — and U+0004 is an
37
+ * invisible control character on a terminal: a key copied from a report
38
+ * could not be pasted back into `--redo keys:`. Every message printed here
39
+ * shows it as "␄" (U+2404), which `--redo keys:` / `--force-keys` accept
40
+ * back (lib/locale-layout.js keysForNamespace). Structured `data` fields in
41
+ * --json keep the exact key.
42
+ *
43
+ * @param {*} msg
44
+ * @returns {*} msg with U+0004 shown as ␄ (non-strings unchanged)
45
+ */
46
+ function showKey(msg) {
47
+ return typeof msg === 'string' ? msg.replace(/\u0004/g, '\u2404') : msg;
48
+ }
49
+
50
+ /**
51
+ * LINE-ATOMIC OUTPUT. Locales are translated in parallel, and a progress
52
+ * bar used to be a partial line (`\r…` with no newline) that any other
53
+ * locale's message was appended to mid-line ("Translating 3 key(s) to
54
+ * French (local)...[INFO] ru/django.po — 3 missing"). Now:
55
+ * - every message is one complete line;
56
+ * - a progress bar names its file, and is redrawn in place only on a
57
+ * terminal and only while nothing else has printed since — any other
58
+ * output first ends the open bar line, so nothing is lost or overwritten;
59
+ * - off a terminal (CI logs, pipes) every bar update is its own line.
60
+ *
61
+ * `openBar` is the item whose bar line is currently unterminated.
62
+ */
63
+ let openBar = null;
64
+
65
+ function endOpenBar() {
66
+ if (openBar !== null) {
67
+ process.stdout.write('\n');
68
+ openBar = null;
69
+ }
70
+ }
71
+
34
72
  /**
35
73
  * Set the output mode. Call once during CLI bootstrap.
36
74
  *
@@ -64,10 +102,11 @@ function getMode() {
64
102
  function info(msg, data) {
65
103
  if (mode === 'quiet') return;
66
104
  if (mode === 'json') {
67
- console.log(JSON.stringify({ level: 'info', message: msg, ...data }));
105
+ console.log(JSON.stringify({ level: 'info', message: showKey(msg), ...data }));
68
106
  return;
69
107
  }
70
- console.log(`[INFO] ${msg}`);
108
+ endOpenBar();
109
+ console.log(`[INFO] ${showKey(msg)}`);
71
110
  }
72
111
 
73
112
  /**
@@ -80,10 +119,11 @@ function info(msg, data) {
80
119
  function ok(msg, data) {
81
120
  if (mode === 'quiet') return;
82
121
  if (mode === 'json') {
83
- console.log(JSON.stringify({ level: 'ok', message: msg, ...data }));
122
+ console.log(JSON.stringify({ level: 'ok', message: showKey(msg), ...data }));
84
123
  return;
85
124
  }
86
- console.log(`[OK] ${msg}`);
125
+ endOpenBar();
126
+ console.log(`[OK] ${showKey(msg)}`);
87
127
  }
88
128
 
89
129
  /**
@@ -95,10 +135,11 @@ function ok(msg, data) {
95
135
  */
96
136
  function warn(msg, data) {
97
137
  if (mode === 'json') {
98
- console.error(JSON.stringify({ level: 'warn', message: msg, ...data }));
138
+ console.error(JSON.stringify({ level: 'warn', message: showKey(msg), ...data }));
99
139
  return;
100
140
  }
101
- console.error(`[WARN] ${msg}`);
141
+ endOpenBar();
142
+ console.error(`[WARN] ${showKey(msg)}`);
102
143
  }
103
144
 
104
145
  /**
@@ -110,21 +151,48 @@ function warn(msg, data) {
110
151
  */
111
152
  function error(msg, data) {
112
153
  if (mode === 'json') {
113
- console.error(JSON.stringify({ level: 'error', message: msg, ...data }));
154
+ console.error(JSON.stringify({ level: 'error', message: showKey(msg), ...data }));
114
155
  return;
115
156
  }
116
- console.error(`[ERR] ${msg}`);
157
+ endOpenBar();
158
+ console.error(`[ERR] ${showKey(msg)}`);
117
159
  }
118
160
 
119
161
  /**
120
- * Progress message — inline status for long-running operations.
162
+ * Progress message — status for long-running operations, one complete line
163
+ * (a trailing newline is added; see LINE-ATOMIC OUTPUT above). Callers name
164
+ * what the line is about: with locales running in parallel, a bare
165
+ * " [OK]" could belong to any of them.
121
166
  * Suppressed in quiet and json modes.
122
167
  *
123
168
  * @param {string} msg - Progress description
124
169
  */
125
170
  function progress(msg) {
126
171
  if (mode === 'quiet' || mode === 'json') return;
127
- process.stdout.write(msg);
172
+ const line = showKey(String(msg)).replace(/\n+$/, '');
173
+ if (line.trim() === '') return;
174
+ endOpenBar();
175
+ process.stdout.write(`${line}\n`);
176
+ }
177
+
178
+ /**
179
+ * Finish an item's progress: " [OK]" after its own bar when that bar is the
180
+ * line still open on a terminal, else a complete line naming the item.
181
+ * Suppressed in quiet and json modes.
182
+ *
183
+ * @param {string} item - What finished (the file name the bar showed)
184
+ * @param {string} status - e.g. "[OK]", "[ERR] all translations failed quality gate"
185
+ */
186
+ function progressDone(item, status) {
187
+ if (mode === 'quiet' || mode === 'json') return;
188
+ const label = showKey(String(item));
189
+ if (openBar === label) {
190
+ process.stdout.write(` ${status}\n`);
191
+ openBar = null;
192
+ return;
193
+ }
194
+ endOpenBar();
195
+ process.stdout.write(` ${label} ${status}\n`);
128
196
  }
129
197
 
130
198
  /**
@@ -135,7 +203,24 @@ function progress(msg) {
135
203
  */
136
204
  function debug(msg, data) {
137
205
  if (mode !== 'verbose') return;
138
- console.log(`[DEBUG] ${msg}`);
206
+ endOpenBar();
207
+ console.log(`[DEBUG] ${showKey(msg)}`);
208
+ }
209
+
210
+ /**
211
+ * Event — one machine-readable progress record (json mode only).
212
+ * Emits `{level:'event', event, ...fields}` on stdout; silent otherwise —
213
+ * the human output already says the same thing in prose.
214
+ *
215
+ * Event types used by sync: 'cost' (the pre-run estimate, before the
216
+ * --max-cost gate) and 'file' (one per content file × locale processed).
217
+ *
218
+ * @param {string} event - Event type
219
+ * @param {object} [fields] - Structured data
220
+ */
221
+ function event(eventType, fields) {
222
+ if (mode !== 'json') return;
223
+ console.log(JSON.stringify({ level: 'event', event: eventType, ...fields }));
139
224
  }
140
225
 
141
226
  /**
@@ -153,6 +238,7 @@ function summary(data) {
153
238
  return;
154
239
  }
155
240
  if (data.title) {
241
+ endOpenBar();
156
242
  console.log(`\n${data.title}`);
157
243
  }
158
244
  }
@@ -165,14 +251,18 @@ function summary(data) {
165
251
  */
166
252
  function banner(version) {
167
253
  if (mode === 'quiet' || mode === 'json') return;
254
+ endOpenBar();
168
255
  console.log(`champollion v${version}\n`);
169
256
  }
170
257
 
171
258
  /**
172
- * Progress bar — inline key-count progress indicator.
259
+ * Progress bar — key-count progress for one item (a locale file).
173
260
  *
174
- * Renders a bar like: ` ████████░░░░░░░░ 1,440/2,847 keys`
175
- * Uses carriage return (\r) to overwrite the current line in-place.
261
+ * Renders a line like: ` fr.json ████████░░░░░░░░ 1,440/2,847 keys`
262
+ * On a terminal the line is redrawn in place (\r) while it is still the
263
+ * last thing printed and belongs to the same item; otherwise (another
264
+ * locale's bar, any other message, or output that is not a terminal) each
265
+ * update is its own complete line — see LINE-ATOMIC OUTPUT.
176
266
  * Call with `done = true` to finalize the line with a newline.
177
267
  *
178
268
  * Suppressed in quiet and json modes.
@@ -180,12 +270,14 @@ function banner(version) {
180
270
  * @param {number} completed - Number of items completed
181
271
  * @param {number} total - Total number of items
182
272
  * @param {object} [opts] - Options
273
+ * @param {string} [opts.item=''] - What the bar is for (e.g. the locale file)
183
274
  * @param {string} [opts.label='keys'] - Unit label
184
275
  * @param {boolean} [opts.done=false] - Whether this is the final update (appends newline)
185
276
  */
186
277
  function progressBar(completed, total, opts = {}) {
187
278
  if (mode === 'quiet' || mode === 'json') return;
188
279
  const { label = 'keys', done = false } = opts;
280
+ const item = showKey(String(opts.item || ''));
189
281
 
190
282
  // Calculate bar width based on terminal width, with sane defaults
191
283
  const termWidth = process.stdout.columns || 80;
@@ -195,12 +287,34 @@ function progressBar(completed, total, opts = {}) {
195
287
  const filled = Math.round(ratio * barWidth);
196
288
  const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
197
289
  const nums = `${completed.toLocaleString()}/${total.toLocaleString()} ${label}`;
290
+ const line = ` ${item ? `${item} ` : ''}${bar} ${nums}`;
198
291
 
199
- if (done) {
200
- process.stdout.write(`\r ${bar} ${nums}\n`);
292
+ if (!process.stdout.isTTY) {
293
+ endOpenBar();
294
+ process.stdout.write(`${line}\n`);
295
+ return;
296
+ }
297
+ if (openBar === item) {
298
+ process.stdout.write(`\r${line}`);
201
299
  } else {
202
- process.stdout.write(`\r ${bar} ${nums}`);
300
+ endOpenBar();
301
+ process.stdout.write(line);
203
302
  }
303
+ openBar = item;
304
+ if (done) endOpenBar();
305
+ }
306
+
307
+ /**
308
+ * A warning block: pre-formatted lines on stderr (the quality-gate and
309
+ * glossary reports), each a complete line with keys shown copyably.
310
+ * Emitted in every human mode, like warn(); callers emit structured
311
+ * records themselves in json mode.
312
+ *
313
+ * @param {string[]} lines
314
+ */
315
+ function block(lines) {
316
+ endOpenBar();
317
+ for (const line of lines) console.error(showKey(line));
204
318
  }
205
319
 
206
320
  /**
@@ -217,7 +331,8 @@ function progressBar(completed, total, opts = {}) {
217
331
  */
218
332
  function raw(msg) {
219
333
  if (mode === 'quiet' || mode === 'json') return;
220
- console.log(msg);
334
+ endOpenBar();
335
+ console.log(showKey(msg));
221
336
  }
222
337
 
223
338
  const output = {
@@ -228,11 +343,37 @@ const output = {
228
343
  warn,
229
344
  error,
230
345
  progress,
346
+ progressDone,
231
347
  debug,
232
348
  summary,
349
+ event,
233
350
  banner,
234
351
  progressBar,
235
352
  raw,
353
+ block,
236
354
  };
237
355
 
238
- export { output };
356
+ /**
357
+ * A stored ISO timestamp as this machine's local date and time, with the
358
+ * zone named: "2026-10-03 18:20 MDT". The caches store UTC (`…Z`); printing
359
+ * the UTC date bare read as a day ahead of local time in the evening
360
+ * (Round 5, Next.js persona: `tm stats` "Created"). Zones without an
361
+ * abbreviation print as an offset ("GMT+5:30"). An unparsable value is
362
+ * returned unchanged; a missing one as "unknown".
363
+ *
364
+ * @param {string|null|undefined} iso
365
+ * @param {{ timeZone?: string }} [opts] - For tests; defaults to the machine's zone
366
+ * @returns {string}
367
+ */
368
+ function localTimestamp(iso, opts = {}) {
369
+ if (!iso) return 'unknown';
370
+ const d = new Date(iso);
371
+ if (Number.isNaN(d.getTime())) return String(iso);
372
+ const parts = Object.fromEntries(new Intl.DateTimeFormat('en-US', {
373
+ year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
374
+ hourCycle: 'h23', timeZoneName: 'short', ...(opts.timeZone && { timeZone: opts.timeZone }),
375
+ }).formatToParts(d).map(p => [p.type, p.value]));
376
+ return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute} ${parts.timeZoneName}`;
377
+ }
378
+
379
+ export { output, showKey, localTimestamp };