champollion 0.3.3 → 0.4.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 (142) hide show
  1. package/README.md +52 -37
  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 +83 -36
  6. package/lib/bridge/method_bridge.py +15 -3
  7. package/lib/cards/reader.js +51 -3
  8. package/lib/cards/remote.js +15 -0
  9. package/lib/cards/search-names.js +178 -0
  10. package/lib/command-help.js +289 -88
  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 +1689 -87
  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 +649 -130
  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 +16 -10
  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 +196 -14
  32. package/lib/content-estimate.js +96 -0
  33. package/lib/content-refusals.js +270 -0
  34. package/lib/content-review.js +372 -0
  35. package/lib/content-sync.js +1127 -344
  36. package/lib/content.js +94 -7
  37. package/lib/corpus-registration.mjs +197 -38
  38. package/lib/cost-label.js +29 -0
  39. package/lib/cost-report.js +726 -78
  40. package/lib/diff.js +38 -4
  41. package/lib/docusaurus-sync.js +965 -253
  42. package/lib/edit-distance.js +31 -0
  43. package/lib/fallback.js +964 -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 +571 -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 +252 -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 +68 -128
  70. package/lib/methods/llm.js +80 -31
  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 +11 -0
  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 +193 -106
  100. package/lib/seal.mjs +6 -5
  101. package/lib/sealed-qualifier.mjs +2 -2
  102. package/lib/segment.js +2 -1
  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 +880 -17
  117. package/lib/verify.js +1296 -104
  118. package/lib/watch.js +32 -13
  119. package/lib/xliff.js +44 -3
  120. package/package.json +3 -2
  121. package/shared/CORPORA-CARDS.md +2 -0
  122. package/shared/DATA-SOVEREIGNTY.md +19 -20
  123. package/shared/LANGUAGE-CARD-FIELDS.md +1 -1
  124. package/shared/cards-fallback.json +1 -1
  125. package/shared/catalogue/card-config.json +1 -1
  126. package/shared/curated-orthography-conventions.json +26 -8
  127. package/shared/docent/faq.en.json +14 -16
  128. package/shared/docent/system-prompt.md +17 -19
  129. package/shared/explainers/tc-features.json +15 -15
  130. package/shared/gettext-plural-forms.json +45 -0
  131. package/shared/human-services.json +1 -1
  132. package/shared/method-registry.json +2 -0
  133. package/shared/metric-registry.json +96 -18
  134. package/shared/schemas/champollion-plugin.schema.json +4 -0
  135. package/shared/schemas/corpora-card.schema.json +20 -10
  136. package/shared/schemas/human-services.schema.json +2 -2
  137. package/shared/schemas/language-card.schema.json +1 -1
  138. package/shared/schemas/method-card.schema.json +1 -1
  139. package/shared/schemas/method-index-record.schema.json +67 -0
  140. package/shared/schemas/method-registry.schema.json +4 -0
  141. package/shared/schemas/metric-registry.schema.json +55 -1
  142. package/shared/docent/corpus.json +0 -11333
package/lib/translate.js CHANGED
@@ -41,6 +41,8 @@ import { ExternalMethod } from './methods/external.js';
41
41
  import { DEFAULT_OPENROUTER_MODEL } from './config.js';
42
42
  import { manifestEntries, cliNameFor } from './method-manifest.js';
43
43
  import { assertRoutable } from './commercial-eligibility.js';
44
+ import { CONTENT_SEPARATOR, parseContentPrompt } from './methods/content-separator.js';
45
+ import { SEGMENT_MARKER_PREFIX, SEGMENT_MARKER_SUFFIX } from './segment.js';
44
46
 
45
47
  /**
46
48
  * Registry of available translation methods.
@@ -69,6 +71,27 @@ const METHOD_REGISTRY = {
69
71
  'external': ExternalMethod,
70
72
  };
71
73
 
74
+ /**
75
+ * ExternalMethod instances by plugin path. Each instance owns a Python bridge
76
+ * process, so building one per call (getMethod runs for every batch, every
77
+ * preflight, every content file) started a new process each time and never
78
+ * stopped any. One instance per plugin is reused for the whole run and
79
+ * stopped by shutdownMethods().
80
+ */
81
+ const externalMethods = new Map();
82
+
83
+ /**
84
+ * Stop every bridge process started through getMethod(). Call when a run is
85
+ * finished; a later getMethod() starts a fresh bridge on demand.
86
+ *
87
+ * @returns {Promise<void>}
88
+ */
89
+ async function shutdownMethods() {
90
+ const instances = [...externalMethods.values()];
91
+ externalMethods.clear();
92
+ await Promise.all(instances.map(m => m.shutdown()));
93
+ }
94
+
72
95
  /**
73
96
  * Find a shared-registry entry that exists but is NOT served in the CLI runtime.
74
97
  *
@@ -153,14 +176,27 @@ function getMethod(methodName, pluginContext) {
153
176
  methodVersion: pluginContext.pluginVersion,
154
177
  qualityTier: pluginContext.qualityTier,
155
178
  provenance: pluginContext.pluginProvenance,
179
+ acceptsInstructions: pluginContext.acceptsInstructions,
156
180
  });
157
181
  }
158
182
 
159
- // ExternalMethod needs methodPath to find the Python plugin directory
183
+ // LLMCoachedMethod needs the pair's transport so preflight checks the
184
+ // credential the run will actually use (OPENAI_API_KEY, not OpenRouter's).
185
+ if (methodName === 'llm-coached') {
186
+ return new MethodClass({ provider: pluginContext?.provider });
187
+ }
188
+
189
+ // ExternalMethod needs methodPath to find the Python plugin directory.
190
+ // Shared per plugin so the run reuses one bridge process (see above).
160
191
  if (methodName === 'external') {
161
- return new MethodClass({
162
- methodPath: pluginContext?.methodPath || null,
163
- });
192
+ const methodPath = pluginContext?.methodPath || null;
193
+ if (!methodPath) return new MethodClass({ methodPath });
194
+ let instance = externalMethods.get(methodPath);
195
+ if (!instance) {
196
+ instance = new MethodClass({ methodPath });
197
+ externalMethods.set(methodPath, instance);
198
+ }
199
+ return instance;
164
200
  }
165
201
 
166
202
  return new MethodClass();
@@ -197,7 +233,44 @@ async function translateRawContent(prompt, options) {
197
233
  // (api → endpoint, external → methodPath) are constructed the same
198
234
  // way here as on the key-value path in translateBatch.
199
235
  const method = getMethod(pairConfig.method, pairConfig);
236
+ if (method.translatesRawText) {
237
+ return translateBlocksForRawTextEngine(method, prompt, pairConfig, options);
238
+ }
200
239
  return method.translateContent(prompt, pairConfig, options);
201
240
  }
202
241
 
203
- export { translateBatch, translateRawContent, buildPrompt, isUnsafeKey, inferKeyTypes, getMethod, METHOD_REGISTRY };
242
+ /**
243
+ * A raw-text engine gets only Markdown, one block at a time.
244
+ *
245
+ * Those engines read the body after "\n---\n"; the default BLOCK-batch
246
+ * prompt has no such separator, so they were sent — and billed for — the
247
+ * whole LLM instruction prompt with every block in it, and the ⟦SEG_N⟧
248
+ * markers survived only by luck (found 2026-10-03). Each block now goes as
249
+ * its own page-shaped prompt and the reply is reassembled under its marker;
250
+ * a block the engine fails is simply absent, which the content gate reports.
251
+ */
252
+ async function translateBlocksForRawTextEngine(method, prompt, pairConfig, options) {
253
+ const request = parseContentPrompt(prompt);
254
+ if (!request) return null; // nothing but instructions — never send those
255
+ if (request.mode === 'page') {
256
+ return method.translateContent(`${CONTENT_SEPARATOR}${request.keys.body}`, pairConfig, options);
257
+ }
258
+ const translated = new Map();
259
+ const queue = [...request.ids];
260
+ const worker = async () => {
261
+ while (queue.length > 0) {
262
+ const id = queue.shift();
263
+ const text = request.keys[`segment.${id}`];
264
+ if (!text || !text.trim()) { translated.set(id, text ?? ''); continue; }
265
+ const out = await method.translateContent(`${CONTENT_SEPARATOR}${text}`, pairConfig, options);
266
+ if (typeof out === 'string') translated.set(id, out);
267
+ }
268
+ };
269
+ await Promise.all(Array.from({ length: Math.min(4, request.ids.length) }, worker));
270
+ const parts = request.ids
271
+ .filter((id) => translated.has(id))
272
+ .map((id) => `${SEGMENT_MARKER_PREFIX}${id}${SEGMENT_MARKER_SUFFIX}\n${translated.get(id)}`);
273
+ return parts.length > 0 ? parts.join('\n\n') : null;
274
+ }
275
+
276
+ export { translateBatch, translateRawContent, translateBlocksForRawTextEngine, buildPrompt, isUnsafeKey, inferKeyTypes, getMethod, shutdownMethods, METHOD_REGISTRY };
package/lib/types.js CHANGED
@@ -23,8 +23,11 @@
23
23
  * @property {number} version - Config schema version (currently 3)
24
24
  * @property {string} inputLocale - Source locale code (e.g., 'en')
25
25
  * @property {string} baseUrl - Site base URL for SEO commands
26
- * @property {string} localesDir - Absolute path to locale files directory
27
- * @property {string|null} contentDir - Hugo/Docusaurus content directory, or null if disabled
26
+ * @property {string} localesDir - Absolute path to locale files directory (derived from localesPattern when that is set)
27
+ * @property {string|null} [localesPattern] - Absolute locale-file pattern with {lang} and optional {ns} (lib/locale-layout.js)
28
+ * @property {'flat'|'dir'|null} [localesLayout] - Forces the layout auto-detection's answer
29
+ * @property {string|null} [defaultNamespace] - Namespace file `wrap` adds keys to in a multi-file locale
30
+ * @property {string|null} contentDir - Folder of Markdown/MDX to translate (a Hugo content/ dir or any folder), or null if disabled
28
31
  * @property {string[]|null} translatableFields - Override for content translatable fields, or null for defaults
29
32
  * @property {Array<string>|object} languages - Target languages (array of codes or object with config)
30
33
  * @property {Object<string, LanguageConfig>} [resolvedLanguages] - Fully resolved language map (code → config). Set by resolveConfig.
@@ -33,6 +36,7 @@
33
36
  * @property {object|null} pairs - Advanced per-pair overrides, or null
34
37
  * @property {string} model - Default translation model (e.g., 'openai/gpt-4o-mini')
35
38
  * @property {string} defaultMethod - Global default method: 'llm', 'google-translate', 'api'
39
+ * @property {string|null} [provider] - LLM-lane transport: 'openrouter' (default), 'openai', 'anthropic', 'gemini', 'local'
36
40
  * @property {number} batchSize - Default batch size for translation calls
37
41
  * @property {string} fallbackPrefix - Prefix for untranslated fallback values (default: '[EN] ')
38
42
  * @property {string} apiKeyEnvVar - Environment variable name for the API key
@@ -54,6 +58,7 @@
54
58
  * @property {string} source - Source locale code (e.g., 'en')
55
59
  * @property {string} target - Target locale code (e.g., 'fr')
56
60
  * @property {string} method - Translation method name: 'llm', 'llm-coached', 'google-translate', 'api'
61
+ * @property {string|null} provider - Resolved transport for llm-coached / a rewritten llm pair; null for engine methods
57
62
  * @property {string} model - Model identifier (e.g., 'openai/gpt-4o-mini')
58
63
  * @property {string} qualityTier - Quality tier: 'standard', 'high', 'research', 'verified'
59
64
  * @property {number} batchSize - Max keys per API call
@@ -70,6 +75,8 @@
70
75
  * @property {string|null} formalitySystem - Formality system name (e.g., 'T-V', 'speech-levels'). Used by DeepL for structured formality mapping.
71
76
  * @property {string|null} genderGuidance - Language-specific gender guidance for LLM prompts, or null
72
77
  * @property {Set<string>} _defaults - Fields that were filled from system defaults (used by plugin precedence)
78
+ * @property {PairConfig} [fallback] - The second method for what this pair's own method cannot translate safely (set by resolvePairs when configured; see pairs.js resolveFallbackForPair). Itself a full PairConfig with `isFallback: true` and no `fallback` of its own.
79
+ * @property {boolean} [isFallback] - True on a resolved fallback config
73
80
  *
74
81
  * --- Plugin-injected fields (present after resolvePluginForPair) ---
75
82
  * @property {string} [endpoint] - Plugin API endpoint URL
@@ -100,9 +107,18 @@
100
107
  * @property {string} [dir] - Text direction: 'ltr' or 'rtl'
101
108
  * @property {string|null} [formalitySystem] - Formality system name (e.g., 'T-V', 'speech-levels', 'keigo')
102
109
  * @property {string} [model] - Per-language model override
110
+ * @property {string} [provider] - Per-language LLM-lane transport override
103
111
  * @property {number} [batchSize] - Per-language batch size override
104
112
  * @property {number} [maxRetries] - Per-language max retries override
105
113
  * @property {string} [script] - Script system override (e.g., 'syllabics' for Cree)
114
+ * @property {string} [method] - Per-language translation method
115
+ * @property {string} [endpoint] - API endpoint for the `api` method
116
+ * @property {number} [temperature] - Per-language temperature
117
+ * @property {string} [coachingFile] - Per-language coaching prompt file
118
+ * @property {string} [coachingPrompt] - Per-language coaching prompt text
119
+ * @property {string} [promptContext] - Per-language application context
120
+ * @property {'block'|'page'} [contentSegmentation] - Markdown body granularity
121
+ * @property {object|null} [fallback] - Raw `fallback` (a second method), resolved by pairs.js
106
122
  */
107
123
 
108
124
  // -----------------------------------------------------------------
@@ -218,10 +234,13 @@
218
234
  * @property {string} [format] - Locale file format override
219
235
  * @property {string} [out] - Output file path (seo sitemap)
220
236
  * @property {string} [src] - Source directory for lint/wrap
221
- * @property {string} ['content-dir'] - Hugo content directory
237
+ * @property {string} ['content-dir'] - Folder of Markdown/MDX to translate (overrides contentDir)
222
238
  * @property {string} ['base-url'] - Site base URL override
223
239
  * @property {string} ['min-length'] - Minimum string length to flag
224
240
  * @property {string} ['force-keys'] - Comma-separated keys to force re-translate
241
+ * @property {boolean} ['fresh-on-model-change'] - Serve only exact-model TM hits (default reuses other models' cached translations)
242
+ * @property {string[]} [files] - sync: only these content files (globs)
243
+ * @property {string[]} [retranslate] - sync: translate these content files fresh (globs)
225
244
  * @property {boolean} ['warn-only'] - Exit 0 even with issues
226
245
  */
227
246