champollion 0.3.3

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 (170) hide show
  1. package/LICENSE +133 -0
  2. package/README.md +387 -0
  3. package/bin/cli.js +278 -0
  4. package/index.js +135 -0
  5. package/lib/api-key.js +127 -0
  6. package/lib/autofix.js +432 -0
  7. package/lib/bridge/method_bridge.py +430 -0
  8. package/lib/card-source-resolution.mjs +284 -0
  9. package/lib/cards/cache.js +169 -0
  10. package/lib/cards/env.js +82 -0
  11. package/lib/cards/fetch-card-child.js +38 -0
  12. package/lib/cards/reader.js +435 -0
  13. package/lib/cards/refresh.js +111 -0
  14. package/lib/cards/remote.js +387 -0
  15. package/lib/cldf-export.mjs +540 -0
  16. package/lib/cldf-terms.mjs +62 -0
  17. package/lib/command-help.js +790 -0
  18. package/lib/commands/audit.js +49 -0
  19. package/lib/commands/card.js +454 -0
  20. package/lib/commands/doctor.js +559 -0
  21. package/lib/commands/fonts.js +489 -0
  22. package/lib/commands/help.js +91 -0
  23. package/lib/commands/init.js +1259 -0
  24. package/lib/commands/integrity.js +148 -0
  25. package/lib/commands/leaderboard.js +478 -0
  26. package/lib/commands/lint.js +30 -0
  27. package/lib/commands/models.js +177 -0
  28. package/lib/commands/plugin.js +103 -0
  29. package/lib/commands/provenance.js +45 -0
  30. package/lib/commands/recommend.js +75 -0
  31. package/lib/commands/register-corpus.js +678 -0
  32. package/lib/commands/repair-script.js +42 -0
  33. package/lib/commands/seal-corpus.js +355 -0
  34. package/lib/commands/seo.js +72 -0
  35. package/lib/commands/serve.js +147 -0
  36. package/lib/commands/status.js +265 -0
  37. package/lib/commands/submit.js +332 -0
  38. package/lib/commands/sync.js +89 -0
  39. package/lib/commands/tm.js +573 -0
  40. package/lib/commands/verify.js +39 -0
  41. package/lib/commands/watch.js +20 -0
  42. package/lib/commands/wrap.js +138 -0
  43. package/lib/commands/xliff.js +327 -0
  44. package/lib/commercial-eligibility.js +235 -0
  45. package/lib/concurrent.js +87 -0
  46. package/lib/config.js +523 -0
  47. package/lib/contamination-lane.js +76 -0
  48. package/lib/content-sync.js +731 -0
  49. package/lib/content.js +733 -0
  50. package/lib/corpus-registration.mjs +608 -0
  51. package/lib/cost-report.js +346 -0
  52. package/lib/diff.js +155 -0
  53. package/lib/docusaurus-sync.js +1256 -0
  54. package/lib/flatten.js +55 -0
  55. package/lib/format.js +954 -0
  56. package/lib/hash.js +159 -0
  57. package/lib/icu.js +473 -0
  58. package/lib/integrity.js +689 -0
  59. package/lib/license-gate.mjs +478 -0
  60. package/lib/license-identify.mjs +229 -0
  61. package/lib/lint.js +629 -0
  62. package/lib/method-manifest.js +60 -0
  63. package/lib/methods/anthropic.js +140 -0
  64. package/lib/methods/apertium.js +163 -0
  65. package/lib/methods/api.js +316 -0
  66. package/lib/methods/base.js +184 -0
  67. package/lib/methods/content-separator.js +45 -0
  68. package/lib/methods/deepl.js +426 -0
  69. package/lib/methods/direct-llm.js +586 -0
  70. package/lib/methods/external.js +332 -0
  71. package/lib/methods/fetch-with-retry.js +124 -0
  72. package/lib/methods/gemini.js +147 -0
  73. package/lib/methods/google-translate.js +402 -0
  74. package/lib/methods/http-utils.js +122 -0
  75. package/lib/methods/libretranslate.js +314 -0
  76. package/lib/methods/llm-coached.js +670 -0
  77. package/lib/methods/llm.js +592 -0
  78. package/lib/methods/local.js +76 -0
  79. package/lib/methods/microsoft-translator.js +331 -0
  80. package/lib/methods/openai.js +131 -0
  81. package/lib/methods/openrouter-client.js +327 -0
  82. package/lib/methods/openrouter-pricing.js +156 -0
  83. package/lib/methods/provider-env.js +115 -0
  84. package/lib/methods/provider-pricing.js +310 -0
  85. package/lib/methods/tilde.js +150 -0
  86. package/lib/methods/translated.js +229 -0
  87. package/lib/methods/translation-error.js +80 -0
  88. package/lib/models.js +258 -0
  89. package/lib/no-translate.js +233 -0
  90. package/lib/output.js +238 -0
  91. package/lib/pairs.js +547 -0
  92. package/lib/plugins.js +447 -0
  93. package/lib/provenance.js +323 -0
  94. package/lib/recommend.js +648 -0
  95. package/lib/registers.js +1185 -0
  96. package/lib/repair-script.js +266 -0
  97. package/lib/scripts.js +994 -0
  98. package/lib/seal.mjs +464 -0
  99. package/lib/sealed-qualifier.mjs +211 -0
  100. package/lib/security.js +59 -0
  101. package/lib/segment.js +369 -0
  102. package/lib/seo.js +275 -0
  103. package/lib/serve.js +854 -0
  104. package/lib/string-classify.js +85 -0
  105. package/lib/submit.mjs +344 -0
  106. package/lib/sync.js +969 -0
  107. package/lib/tags/bcp47.js +202 -0
  108. package/lib/tags/resolve.js +314 -0
  109. package/lib/terminology.js +111 -0
  110. package/lib/tm-seed.js +294 -0
  111. package/lib/tm.js +515 -0
  112. package/lib/translate-pair.js +197 -0
  113. package/lib/translate.js +203 -0
  114. package/lib/types.js +230 -0
  115. package/lib/validate.js +510 -0
  116. package/lib/verify.js +451 -0
  117. package/lib/watch.js +145 -0
  118. package/lib/xliff.js +184 -0
  119. package/package.json +93 -0
  120. package/shared/ATTRIBUTION.md +145 -0
  121. package/shared/CORPORA-CARDS.md +288 -0
  122. package/shared/DATA-SOVEREIGNTY.md +500 -0
  123. package/shared/LANGUAGE-CARD-FIELDS.md +532 -0
  124. package/shared/card-lint-baseline.json +3189 -0
  125. package/shared/cards-fallback.json +1 -0
  126. package/shared/catalogue/card-config.json +6091 -0
  127. package/shared/catalogue/external-results.json +3888 -0
  128. package/shared/catalogue/gender-guidance.json +1038 -0
  129. package/shared/catalogue/method-coverage.json +1751 -0
  130. package/shared/catalogue/metric-coverage.json +170 -0
  131. package/shared/catalogue/metric-reliability.json +1 -0
  132. package/shared/catalogue/register-presets.json +3180 -0
  133. package/shared/catalogue/vitality-scales.json +55 -0
  134. package/shared/cldr-index.json +1115 -0
  135. package/shared/code-bridge.json +253 -0
  136. package/shared/corpora-cards-v1-reference.md +281 -0
  137. package/shared/curated-dictionary-flags.json +35 -0
  138. package/shared/curated-endonyms.json +35 -0
  139. package/shared/curated-fsts.json +51 -0
  140. package/shared/curated-orthography-conventions.json +26 -0
  141. package/shared/curated-sil-resources.json +374 -0
  142. package/shared/curated-tools.json +41 -0
  143. package/shared/docent/corpus.json +11333 -0
  144. package/shared/docent/faq.en.json +564 -0
  145. package/shared/docent/register-blocks.json +60 -0
  146. package/shared/docent/system-prompt.md +144 -0
  147. package/shared/domain-taxonomy.json +35 -0
  148. package/shared/explainers/glossary.json +2975 -0
  149. package/shared/explainers/tc-features.json +20112 -0
  150. package/shared/explainers/term-watchlist.json +147 -0
  151. package/shared/human-services.json +59 -0
  152. package/shared/license-corrections.json +261 -0
  153. package/shared/license-evidence.json +13452 -0
  154. package/shared/licenses.json +6781 -0
  155. package/shared/method-registry.json +236 -0
  156. package/shared/metric-registry.json +620 -0
  157. package/shared/model-aliases.json +7 -0
  158. package/shared/schemas/champollion-plugin.schema.json +206 -0
  159. package/shared/schemas/corpora-card.schema.json +957 -0
  160. package/shared/schemas/domain-taxonomy.schema.json +64 -0
  161. package/shared/schemas/external-results.schema.json +314 -0
  162. package/shared/schemas/human-services.schema.json +90 -0
  163. package/shared/schemas/language-card.schema.json +1308 -0
  164. package/shared/schemas/licenses.schema.json +155 -0
  165. package/shared/schemas/method-card.schema.json +412 -0
  166. package/shared/schemas/method-registry.schema.json +85 -0
  167. package/shared/schemas/metric-registry.schema.json +96 -0
  168. package/shared/schemas/metric-reliability.schema.json +178 -0
  169. package/shared/schemas/model-aliases.schema.json +27 -0
  170. package/shared/schemas/source-snapshot.schema.json +96 -0
@@ -0,0 +1,197 @@
1
+ /**
2
+ * translate-pair.js — Shared translation pipeline for a single pair
3
+ *
4
+ * Encapsulates the common sequence used by both the standard sync path
5
+ * and the Docusaurus sync path:
6
+ *
7
+ * 1. TM partition: split keys into cached hits and API misses
8
+ * 2. API call: translate the misses via translateBatch
9
+ * 3. Quality gate: validate translations (hallucination, script, length)
10
+ * — TM hits that fail are evicted so the poison cannot be re-served
11
+ * — gate failures get ONE feedback retry (the rejection reason is
12
+ * injected into the prompt; a blind retry at temperature 0 would
13
+ * return byte-identical output)
14
+ * 4. TM store: cache only gate-validated API translations
15
+ *
16
+ * ORDER MATTERS: the TM must only ever hold gate-validated values. An
17
+ * earlier version stored API output before validation; one degenerate
18
+ * response (e.g. "for translating" → "吗") then poisoned the cache — every
19
+ * later sync served it, failed the gate, and never consulted the API again.
20
+ *
21
+ * Callers get back a structured result and handle their own control flow
22
+ * (fallback decisions, error messages, script conversion, etc.) because
23
+ * those details differ between sync paths.
24
+ */
25
+
26
+ import { translateBatch } from './translate.js';
27
+ import { validateTranslations, logGateFailures } from './validate.js';
28
+ import { partitionByTM, storeTM, evictTM, tmMethodKey } from './tm.js';
29
+ import { output } from './output.js';
30
+
31
+ /**
32
+ * Translate a set of string keys through the TM + API + quality gate pipeline.
33
+ *
34
+ * @param {string[]} stringKeys - Keys to translate (must be string-valued in sourceFlat)
35
+ * @param {object} sourceFlat - Full flattened source locale map
36
+ * @param {object} pairConfig - Pair configuration (target, method, model, batchSize, etc.)
37
+ * @param {string} pairKey - Human-readable pair identifier for logging (e.g. "en→fr")
38
+ * @param {object} options
39
+ * @param {string} options.apiKey - API key for translation provider
40
+ * @param {object} options.tm - Translation Memory object (mutable — entries are stored in-place)
41
+ * @param {string} options.targetCode - Target language code
42
+ * @param {object} [options.descriptions] - Optional key descriptions (Docusaurus format)
43
+ * @param {Function} [options.onProgress] - Progress callback: (completed, total) => void
44
+ * @returns {Promise<TranslateResult>}
45
+ *
46
+ * @typedef {object} TranslateResult
47
+ * @property {object|null} translated - Validated translations, or null if all failed
48
+ * @property {number} tmHitCount - Number of keys served from TM cache
49
+ * @property {Array} failures - Quality gate failures (for caller logging)
50
+ * @property {boolean} apiCalled - Whether the API was actually invoked
51
+ * @property {boolean} apiReturnedNull - Whether the API was called but returned null
52
+ */
53
+ export async function translateAndValidate(stringKeys, sourceFlat, pairConfig, pairKey, options) {
54
+ const { apiKey, tm, targetCode, descriptions } = options;
55
+ const method = pairConfig.method || 'llm';
56
+
57
+ // TM entries are keyed on the FULL method key (method|model|register|coaching),
58
+ // not the bare method name: switching model, register, or coaching must be a
59
+ // cache miss, never a silent re-serve of old-style translations. See tm.js.
60
+ const tmKey = tmMethodKey(pairConfig);
61
+
62
+ // Step 1: TM partition — serve cached hits, identify API misses
63
+ const { hits: tmHits, misses: tmMisses } = partitionByTM(
64
+ tm, sourceFlat, stringKeys, targetCode, tmKey
65
+ );
66
+
67
+ const tmHitCount = Object.keys(tmHits).length;
68
+ if (tmHitCount > 0) {
69
+ output.info(`[TM] ${tmHitCount} key(s) served from cache`);
70
+ }
71
+
72
+ // Start with TM hits as the base
73
+ const translated = { ...tmHits };
74
+ let apiCalled = false;
75
+ let apiReturnedNull = false;
76
+
77
+ // Provenance: keys whose current value came from the API this run.
78
+ // Only these are eligible for TM storage after validation, and only
79
+ // non-API (TM-served) failures need cache eviction.
80
+ const apiKeys = new Set();
81
+
82
+ const batchOptions = {
83
+ apiKey,
84
+ model: pairConfig.model,
85
+ batchSize: pairConfig.batchSize,
86
+ onProgress: options.onProgress || null,
87
+ };
88
+
89
+ // Docusaurus passes descriptions for disambiguation context
90
+ if (descriptions) {
91
+ batchOptions.descriptions = descriptions;
92
+ }
93
+
94
+ // Step 2: API call for misses
95
+ if (tmMisses.length > 0) {
96
+ output.progress(` Translating ${tmMisses.length} key(s) to ${pairConfig.name} (${method})...`);
97
+
98
+ const apiResult = await translateBatch(tmMisses, sourceFlat, pairConfig, batchOptions);
99
+ apiCalled = true;
100
+
101
+ if (apiResult) {
102
+ Object.assign(translated, apiResult);
103
+ for (const k of Object.keys(apiResult)) apiKeys.add(k);
104
+ } else {
105
+ apiReturnedNull = true;
106
+ }
107
+ }
108
+
109
+ // Step 3: Quality gate — validate translations before accepting
110
+ let validated = {};
111
+ let failures = [];
112
+ if (Object.keys(translated).length > 0) {
113
+ const result = validateTranslations(translated, sourceFlat, pairConfig);
114
+ validated = result.validated;
115
+ failures = result.failures;
116
+ }
117
+
118
+ // Step 3a: Evict poisoned TM entries. A TM-served value that fails the
119
+ // gate would otherwise be re-served (and re-fail) on every future sync
120
+ // without the API ever being consulted again.
121
+ for (const f of failures) {
122
+ if (f.key in tmHits && !apiKeys.has(f.key) && typeof sourceFlat[f.key] === 'string') {
123
+ evictTM(tm, sourceFlat[f.key], targetCode, tmKey);
124
+ }
125
+ }
126
+
127
+ // Step 3b: Feedback retry — one corrective round for gate failures.
128
+ // The rejection reason is injected as per-key context so the prompt
129
+ // actually changes; without it, a temperature-0 retry is a no-op.
130
+ //
131
+ // NOT gated on `apiKey`: that is the OpenRouter key, and the retry runs
132
+ // through the pair's OWN method — google-translate/deepl/direct providers
133
+ // carry their own credentials. The old `&& apiKey` guard silently skipped
134
+ // the corrective round for every direct provider, which meant a TM entry
135
+ // evicted by the gate (step 3a) was only re-billed on the NEXT sync — a
136
+ // two-pass heal nobody asked for. A method that genuinely cannot run
137
+ // returns null here and the failures simply stand.
138
+ if (failures.length > 0) {
139
+ const retryKeys = failures
140
+ .map(f => f.key)
141
+ .filter(k => typeof sourceFlat[k] === 'string');
142
+
143
+ if (retryKeys.length > 0) {
144
+ output.progress(` Quality gate rejected ${retryKeys.length} key(s) — retrying with feedback...`);
145
+
146
+ const feedbackDescriptions = { ...(descriptions || {}) };
147
+ for (const f of failures) {
148
+ const rejected = String(f.value ?? '').slice(0, 60);
149
+ const base = feedbackDescriptions[f.key] ? `${feedbackDescriptions[f.key]} — ` : '';
150
+ feedbackDescriptions[f.key] =
151
+ `${base}RETRY: a previous attempt ("${rejected}") was rejected by the quality gate: ${f.reason}. ` +
152
+ `Provide a complete, self-contained ${pairConfig.name} translation of this exact string.`;
153
+ }
154
+
155
+ const retryResult = await translateBatch(retryKeys, sourceFlat, pairConfig, {
156
+ ...batchOptions,
157
+ onProgress: null, // avoid double-counting progress
158
+ descriptions: feedbackDescriptions,
159
+ });
160
+ apiCalled = true;
161
+
162
+ if (retryResult) {
163
+ const retryValidation = validateTranslations(retryResult, sourceFlat, pairConfig);
164
+ Object.assign(validated, retryValidation.validated);
165
+ for (const k of Object.keys(retryValidation.validated)) apiKeys.add(k);
166
+
167
+ // Keys that passed on retry are no longer failures; keys the retry
168
+ // produced a fresh (still failing) value for get the newer record.
169
+ const passed = new Set(Object.keys(retryValidation.validated));
170
+ const retryFailureByKey = new Map(retryValidation.failures.map(f => [f.key, f]));
171
+ failures = failures
172
+ .filter(f => !passed.has(f.key))
173
+ .map(f => retryFailureByKey.get(f.key) || f);
174
+ }
175
+ }
176
+ }
177
+
178
+ if (failures.length > 0) {
179
+ logGateFailures(failures, pairKey);
180
+ }
181
+
182
+ // Step 4: TM store — only gate-validated values that came from the API.
183
+ // TM hits are already cached; unvalidated output must never enter the TM.
184
+ for (const [k, v] of Object.entries(validated)) {
185
+ if (apiKeys.has(k) && typeof v === 'string' && typeof sourceFlat[k] === 'string') {
186
+ storeTM(tm, sourceFlat[k], targetCode, tmKey, v);
187
+ }
188
+ }
189
+
190
+ return {
191
+ translated: Object.keys(validated).length > 0 ? validated : null,
192
+ tmHitCount,
193
+ failures,
194
+ apiCalled,
195
+ apiReturnedNull,
196
+ };
197
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Translation orchestrator — delegates to method-specific implementations.
3
+ *
4
+ * v3 ARCHITECTURE:
5
+ * In v2, this module contained all the OpenRouter API logic directly.
6
+ * In v3, the actual API calls live in lib/methods/llm.js (and future
7
+ * method implementations). This module is now the orchestrator:
8
+ *
9
+ * 1. Receives a pair config (from pairs.js) with method/model/qualityTier
10
+ * 2. Instantiates the correct TranslationMethod subclass
11
+ * 3. Delegates the translation call
12
+ * 4. Returns the result
13
+ *
14
+ * This separation means adding a new translation strategy (e.g., fst-gated,
15
+ * human-review) requires only implementing a new method class — zero changes
16
+ * to the sync pipeline or any other consumer.
17
+ *
18
+ * BACKWARD COMPAT:
19
+ * The exported API (translateBatch, translateRawContent, isUnsafeKey) is
20
+ * preserved so that sync.js and content.js continue to work without
21
+ * changes during the transition. The only difference is that translateBatch
22
+ * now accepts an optional pairConfig as the third argument.
23
+ */
24
+
25
+ import { LLMMethod, buildPrompt, inferKeyTypes } from './methods/llm.js';
26
+ import { isUnsafeKey } from './security.js';
27
+ import { LLMCoachedMethod } from './methods/llm-coached.js';
28
+ import { GoogleTranslateMethod } from './methods/google-translate.js';
29
+ import { APIMethod } from './methods/api.js';
30
+ import { DeepLMethod } from './methods/deepl.js';
31
+ import { MicrosoftTranslatorMethod } from './methods/microsoft-translator.js';
32
+ import { LibreTranslateMethod } from './methods/libretranslate.js';
33
+ import { ApertiumMethod } from './methods/apertium.js';
34
+ import { TildeMethod } from './methods/tilde.js';
35
+ import { TranslatedMethod } from './methods/translated.js';
36
+ import { OpenAIMethod } from './methods/openai.js';
37
+ import { AnthropicMethod } from './methods/anthropic.js';
38
+ import { GeminiMethod } from './methods/gemini.js';
39
+ import { LocalMethod } from './methods/local.js';
40
+ import { ExternalMethod } from './methods/external.js';
41
+ import { DEFAULT_OPENROUTER_MODEL } from './config.js';
42
+ import { manifestEntries, cliNameFor } from './method-manifest.js';
43
+ import { assertRoutable } from './commercial-eligibility.js';
44
+
45
+ /**
46
+ * Registry of available translation methods.
47
+ *
48
+ * Each entry maps a method name to its constructor.
49
+ * To add a new method:
50
+ * 1. Create the class in lib/methods/<name>.js
51
+ * 2. Register it here
52
+ * 3. Users can reference it in config pairs: { method: "<name>" }
53
+ */
54
+ const METHOD_REGISTRY = {
55
+ 'llm': LLMMethod,
56
+ 'llm-coached': LLMCoachedMethod,
57
+ 'google-translate': GoogleTranslateMethod,
58
+ 'api': APIMethod,
59
+ 'deepl': DeepLMethod,
60
+ 'microsoft-translator': MicrosoftTranslatorMethod,
61
+ 'libretranslate': LibreTranslateMethod,
62
+ 'apertium': ApertiumMethod,
63
+ 'tilde': TildeMethod,
64
+ 'translated': TranslatedMethod,
65
+ 'openai': OpenAIMethod,
66
+ 'anthropic': AnthropicMethod,
67
+ 'gemini': GeminiMethod,
68
+ 'local': LocalMethod,
69
+ 'external': ExternalMethod,
70
+ };
71
+
72
+ /**
73
+ * Find a shared-registry entry that exists but is NOT served in the CLI runtime.
74
+ *
75
+ * An entry is harness-only when it declares `runtimes` and that list excludes
76
+ * 'cli' (e.g. amazon-translate, local-model). Matches against both the
77
+ * canonical manifest name and the CLI alias (cli_name). Returns null when the
78
+ * manifest is absent (standalone package) or the name is genuinely unknown.
79
+ *
80
+ * @param {string} methodName
81
+ * @returns {{ name: string, entry: object } | null}
82
+ */
83
+ function findHarnessOnlyEntry(methodName) {
84
+ for (const [name, entry] of Object.entries(manifestEntries())) {
85
+ if (name !== methodName && cliNameFor(name, entry) !== methodName) continue;
86
+ if (Array.isArray(entry.runtimes) && !entry.runtimes.includes('cli')) {
87
+ return { name, entry };
88
+ }
89
+ // Found a matching entry that DOES run in the CLI — not the harness-only case.
90
+ return null;
91
+ }
92
+ return null;
93
+ }
94
+
95
+ /**
96
+ * Get a TranslationMethod instance for the given method name.
97
+ *
98
+ * **This is the routing-time licence gate.** Every translation in the CLI is
99
+ * dispatched through here, so it is the one place that can refuse a route.
100
+ * When the caller is operating a COMMERCIAL lane — a paid routing API, a
101
+ * billed `champollion serve` deployment — a method whose licence does not
102
+ * permit commercial use is refused before it can be instantiated, rather
103
+ * than warned about after the fact (lib/provenance.js reports; this
104
+ * enforces). The default lane is non-commercial and is never gated: NC and
105
+ * copyleft engines are legitimate there, and that is the open project.
106
+ *
107
+ * The lane comes from the pair config (`useContext`), so it travels with the
108
+ * route rather than being global state.
109
+ *
110
+ * @param {string} methodName - Method name from pair config
111
+ * @param {import('./types.js').PairConfig} [pluginContext] - Plugin context for API methods
112
+ * @returns {import('./methods/base.js').TranslationMethod} Method instance
113
+ * @throws {CommercialRouteBlockedError} when the lane is commercial and the
114
+ * method is not cleared for it
115
+ */
116
+ function getMethod(methodName, pluginContext) {
117
+ // Gate FIRST: an ineligible method must be refused on licence grounds, not
118
+ // on "unknown method", and must never reach construction.
119
+ assertRoutable(methodName, {
120
+ useContext: pluginContext?.useContext,
121
+ pluginProvenance: pluginContext?.pluginProvenance,
122
+ });
123
+
124
+ const MethodClass = METHOD_REGISTRY[methodName];
125
+ if (!MethodClass) {
126
+ // Before declaring it unknown (which reads like a typo), check whether this
127
+ // is a real, documented engine that simply has no CLI adapter yet. Engines
128
+ // such as `amazon-translate` and `local-model` are declared harness-only in
129
+ // the shared method registry (runtimes: ["harness"]). Surfacing them as
130
+ // "Unknown method" misleads the user into thinking they misspelled it.
131
+ const harnessOnly = findHarnessOnlyEntry(methodName);
132
+ if (harnessOnly) {
133
+ throw new Error(
134
+ `Translation engine "${methodName}" is harness-only — it has no CLI ` +
135
+ `adapter yet, so \`champollion\` cannot run it. Run it with the ` +
136
+ `evaluation harness instead:\n` +
137
+ ` mt-eval run --method ${methodName}`
138
+ );
139
+ }
140
+ const known = Object.keys(METHOD_REGISTRY).join(', ');
141
+ throw new Error(
142
+ `Unknown translation method "${methodName}". ` +
143
+ `Available methods: ${known}. ` +
144
+ `Check your champollion.config.json pairs configuration.`
145
+ );
146
+ }
147
+
148
+ // APIMethod needs plugin context (endpoint, provenance, quality tier)
149
+ if (methodName === 'api' && pluginContext) {
150
+ return new MethodClass({
151
+ endpoint: pluginContext.endpoint,
152
+ methodName: pluginContext.pluginName,
153
+ methodVersion: pluginContext.pluginVersion,
154
+ qualityTier: pluginContext.qualityTier,
155
+ provenance: pluginContext.pluginProvenance,
156
+ });
157
+ }
158
+
159
+ // ExternalMethod needs methodPath to find the Python plugin directory
160
+ if (methodName === 'external') {
161
+ return new MethodClass({
162
+ methodPath: pluginContext?.methodPath || null,
163
+ });
164
+ }
165
+
166
+ return new MethodClass();
167
+ }
168
+
169
+ /**
170
+ * Translate a batch of key-value pairs using the method specified in the pair config.
171
+ *
172
+ * @param {string[]} keys - Flat dot-notation keys to translate
173
+ * @param {object} sourceFlat - Full flattened source locale
174
+ * @param {import('./types.js').PairConfig} pairConfig - Pair config with method, model, register, etc.
175
+ * @param {object} options - { apiKey, model, batchSize }
176
+ * @returns {object|null} Map of key → translated value, or null
177
+ */
178
+ async function translateBatch(keys, sourceFlat, pairConfig, options) {
179
+ const method = getMethod(pairConfig.method || 'llm', pairConfig);
180
+ return method.translate(keys, sourceFlat, pairConfig, options);
181
+ }
182
+
183
+ /**
184
+ * Translate freeform text content (e.g., Markdown body).
185
+ *
186
+ * @param {string} prompt - Complete translation prompt
187
+ * @param {object} options - { apiKey, model } or { apiKey, pairConfig }
188
+ * @returns {string|null} Translated text, or null on failure
189
+ */
190
+ async function translateRawContent(prompt, options) {
191
+ const pairConfig = options.pairConfig || {
192
+ model: options.model || DEFAULT_OPENROUTER_MODEL,
193
+ method: 'llm',
194
+ };
195
+
196
+ // Pass the pair config as plugin context so methods that need it
197
+ // (api → endpoint, external → methodPath) are constructed the same
198
+ // way here as on the key-value path in translateBatch.
199
+ const method = getMethod(pairConfig.method, pairConfig);
200
+ return method.translateContent(prompt, pairConfig, options);
201
+ }
202
+
203
+ export { translateBatch, translateRawContent, buildPrompt, isUnsafeKey, inferKeyTypes, getMethod, METHOD_REGISTRY };
package/lib/types.js ADDED
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Shared type definitions for champollion.
3
+ *
4
+ * This file contains JSDoc @typedef declarations for the core data shapes
5
+ * that flow between modules. It has no runtime code — it exists only so
6
+ * that editors can resolve type references via imports.
7
+ *
8
+ * USAGE IN OTHER MODULES:
9
+ * /** @typedef {import('./types.js').PairConfig} PairConfig * /
10
+ *
11
+ * Or, for VS Code auto-resolution without explicit import, just reference
12
+ * the type name — jsconfig.json's include paths make these globally visible.
13
+ */
14
+
15
+ // -----------------------------------------------------------------
16
+ // ChampollionConfig — the fully resolved project configuration
17
+ // Produced by: config.js:resolveConfig()
18
+ // Consumed by: sync.js, commands/*, seo.js, lint.js, pairs.js
19
+ // -----------------------------------------------------------------
20
+
21
+ /**
22
+ * @typedef {object} ChampollionConfig
23
+ * @property {number} version - Config schema version (currently 3)
24
+ * @property {string} inputLocale - Source locale code (e.g., 'en')
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
28
+ * @property {string[]|null} translatableFields - Override for content translatable fields, or null for defaults
29
+ * @property {Array<string>|object} languages - Target languages (array of codes or object with config)
30
+ * @property {Object<string, LanguageConfig>} [resolvedLanguages] - Fully resolved language map (code → config). Set by resolveConfig.
31
+ * @property {string[]} noTranslate - Dot-path keys / glob patterns whose value is copied verbatim to every locale (e.g. ['**.url']). Never sent to a backend, gated, or billed.
32
+ * @property {boolean} noTranslateUrls - Auto-treat source values that are bare `scheme://` URLs as no-translate (default true)
33
+ * @property {object|null} pairs - Advanced per-pair overrides, or null
34
+ * @property {string} model - Default translation model (e.g., 'openai/gpt-4o-mini')
35
+ * @property {string} defaultMethod - Global default method: 'llm', 'google-translate', 'api'
36
+ * @property {number} batchSize - Default batch size for translation calls
37
+ * @property {string} fallbackPrefix - Prefix for untranslated fallback values (default: '[EN] ')
38
+ * @property {string} apiKeyEnvVar - Environment variable name for the API key
39
+ * @property {string} format - Locale file format: 'json', 'toml', 'yaml', 'auto', 'docusaurus'
40
+ * @property {string[]} [forceKeys] - Dot-notation keys to force re-translate (from --force-keys). Set by resolveConfig.
41
+ * @property {{ srcDir: string|null, ignore: string[], minLength: number }} lint - Lint config
42
+ * @property {{ urlPattern: string, pages: string[]|null }} seo - SEO config
43
+ * @property {{ output: string|null, autoGenerate: boolean }} typegen - Type generation config
44
+ */
45
+
46
+ // -----------------------------------------------------------------
47
+ // PairConfig — a single source→target translation pair
48
+ // Produced by: pairs.js:resolvePairs(), enriched by plugins.js
49
+ // Consumed by: translate.js, methods/*.js, sync.js, provenance.js
50
+ // -----------------------------------------------------------------
51
+
52
+ /**
53
+ * @typedef {object} PairConfig
54
+ * @property {string} source - Source locale code (e.g., 'en')
55
+ * @property {string} target - Target locale code (e.g., 'fr')
56
+ * @property {string} method - Translation method name: 'llm', 'llm-coached', 'google-translate', 'api'
57
+ * @property {string} model - Model identifier (e.g., 'openai/gpt-4o-mini')
58
+ * @property {string} qualityTier - Quality tier: 'standard', 'high', 'research', 'verified'
59
+ * @property {number} batchSize - Max keys per API call
60
+ * @property {number} maxRetries - Max cascade retries on parse failure
61
+ * @property {string} register - Translation register / style instruction (resolved prompt text)
62
+ * @property {string|null} registerPreset - Preset key name (e.g., 'casual-tu') for metadata lookups, or null for custom text
63
+ * @property {string} name - Human-readable language name (e.g., 'French')
64
+ * @property {string} dir - Text direction: 'ltr' or 'rtl'
65
+ * @property {string|null} scripts - LEGACY: the card's scriptConverter key (misleading name, kept for JSON-output compat)
66
+ * @property {string|null} script - User's `script:` config value (ISO 15924), or null
67
+ * @property {object|null} scriptFallback - User transliteration rules for unmapped letters ({ sequence: replacement }), or null
68
+ * @property {{script: string|null, source: string, converterKey: string|null, choices?: Array}} [scriptResolution] - Resolved script decision (set by resolvePairs; see lib/scripts.js resolveTargetScript)
69
+ * @property {string|null} methodPlugin - Plugin name reference, or null
70
+ * @property {string|null} formalitySystem - Formality system name (e.g., 'T-V', 'speech-levels'). Used by DeepL for structured formality mapping.
71
+ * @property {string|null} genderGuidance - Language-specific gender guidance for LLM prompts, or null
72
+ * @property {Set<string>} _defaults - Fields that were filled from system defaults (used by plugin precedence)
73
+ *
74
+ * --- Plugin-injected fields (present after resolvePluginForPair) ---
75
+ * @property {string} [endpoint] - Plugin API endpoint URL
76
+ * @property {string} [pluginName] - Installed plugin name
77
+ * @property {string} [pluginVersion] - Plugin version string
78
+ * @property {string} [pluginDir] - Absolute path to plugin directory
79
+ * @property {object|null} [pluginBenchmarks] - Plugin quality benchmarks, or null
80
+ * @property {object|null} [pluginProvenance] - Plugin provenance/licensing info, or null
81
+ *
82
+ * --- Validation thresholds (used by validate.js quality gate) ---
83
+ * @property {number} [maxLengthRatio] - Max target/source length ratio for validation
84
+ * @property {number} [minLengthRatio] - Min target/source length ratio for validation
85
+ * @property {number} [maxRepetitionRate] - Max character repetition rate threshold
86
+ * @property {boolean} [requireNonLatin] - Whether to require non-Latin script in output
87
+ */
88
+
89
+ // -----------------------------------------------------------------
90
+ // LanguageConfig — per-language settings from the user's config
91
+ // Produced by: config.js (from user's `languages` object)
92
+ // Consumed by: pairs.js:resolvePairs()
93
+ // -----------------------------------------------------------------
94
+
95
+ /**
96
+ * @typedef {object} LanguageConfig
97
+ * @property {string} name - Human-readable language name
98
+ * @property {string} register - Translation register / style instruction (resolved prompt text)
99
+ * @property {string|null} [registerPreset] - Preset key name for metadata lookups (e.g., DeepL formality), or null for custom text
100
+ * @property {string} [dir] - Text direction: 'ltr' or 'rtl'
101
+ * @property {string|null} [formalitySystem] - Formality system name (e.g., 'T-V', 'speech-levels', 'keigo')
102
+ * @property {string} [model] - Per-language model override
103
+ * @property {number} [batchSize] - Per-language batch size override
104
+ * @property {number} [maxRetries] - Per-language max retries override
105
+ * @property {string} [script] - Script system override (e.g., 'syllabics' for Cree)
106
+ */
107
+
108
+ // -----------------------------------------------------------------
109
+ // LanguageCard — unified language metadata from language-cards/*.json
110
+ // Produced by: registers.js:getLanguageCard()
111
+ // Consumed by: config.js, pairs.js, init.js, status.js, deepl.js
112
+ //
113
+ // NOTE (v6 Unified Architecture):
114
+ // All language data lives in a single card file. The former two-tier
115
+ // split (v5) is eliminated. getLanguageReference() is a backward-
116
+ // compat alias for getLanguageCard().
117
+ // -----------------------------------------------------------------
118
+
119
+ /**
120
+ * @typedef {object} LanguageCard
121
+ * @property {string} code - BCP 47 locale code (e.g., 'fr', 'ko', 'x-pirate')
122
+ * @property {string} name - English language name
123
+ * @property {string} nativeName - Name in the language itself (e.g., 'Français', '한국어')
124
+ * @property {string} [iso639_1] - ISO 639-1 two-letter code, or null
125
+ * @property {string} iso639_3 - ISO 639-3 three-letter code
126
+ * @property {string} bcp47 - BCP 47 tag
127
+ * @property {string} script - ISO 15924 script code (e.g., 'Latn', 'Kore', 'Arab')
128
+ * @property {'ltr'|'rtl'} dir - Text directionality
129
+ * @property {LanguageFormality|null} formality - Formality system metadata, or null if no system
130
+ * @property {{ grammatical: boolean, inclusiveGuidance: string|null }} gender - Gender metadata
131
+ * @property {Object<string, RegisterPreset>} registers - Named register presets
132
+ * @property {string[]} aliases - Alternative locale codes that resolve to this card
133
+ * @property {{ googleTranslate: {supported: boolean}, deepl: {supported: boolean, formality?: boolean}, microsoftTranslator: {supported: boolean}, libreTranslate: {supported: boolean}, nllb: {supported: boolean, code?: string}, llm: {supported: boolean} }} methodSupport - API method support — each entry is an object with 'supported' boolean plus optional metadata
134
+ * @property {string|null} scriptConverter - Script converter reference (e.g., 'serbian-latin-cyrillic')
135
+ * @property {string[]} evalDatasets - Dataset IDs for the companion eval harness (not consumed by champollion at runtime)
136
+ * @property {string|null} notes - Free-text notes
137
+ * @property {object|null} rules - Typography, plurals, capitalization, and variable rules (used by compliance plugin)
138
+ * @property {{ reviewed: boolean, reviewer?: string, date?: string }|null} [humanReviewed] - Review status
139
+ * @property {string|null} [glottocode] - Glottolog identifier for family tree cross-referencing
140
+ * @property {string|null} [extends] - Parent card code for inheritance
141
+ * @property {string|null} [macrolanguage] - ISO 639-3 macrolanguage umbrella code (e.g., 'cre', 'ara', 'zho')
142
+ * @property {{ family: string, familyGlottocode: string, genus: string, genusGlottocode: string, ancestry: string[] }|null} [classification] - Genealogical classification from Glottolog + WALS
143
+ * @property {Array<{ source: string, sourceIso639_3?: string, type: string, domains?: string[], depth: string, period?: string, notes?: string }>|null} [contactInfluences] - Universal contact history
144
+ * @property {Array<{ code: string, name: string, primary: boolean }>|null} [scripts] - ISO 15924 script tracking
145
+ * @property {string[]|null} [dataSources] - Provenance tracking (e.g., ['glottolog-5.3', 'cldr-48'])
146
+ * @property {object|null} [linguisticChallenges] - MT-relevant linguistic challenges
147
+ * @property {object|null} [encyclopedic] - Language family, demographics, dialect info
148
+ * @property {object|null} [resources] - NLP corpora, models, FSTs
149
+ * @property {Array<{ country: string, countryCode: string, officialStatus?: string, region?: string, speakerEstimate?: string, coordinates?: [number, number] }>|null} [regions] - Geographic regions where this language is actively spoken
150
+ * @property {{ text: string, transliteration?: string, translation: string, literal?: string, source?: string }|null} [culturalAphorism] - Iconic proverb encapsulating the community's worldview
151
+ */
152
+
153
+ /**
154
+ * @typedef {object} LanguageFormality
155
+ * @property {string} system - Formality system name (e.g., 'T-V', 'speech-levels', 'keigo', 'particles-and-pronouns')
156
+ * @property {string} description - Human-readable description of how formality works in this language
157
+ * @property {string} default - Key of the default register preset
158
+ */
159
+
160
+ /**
161
+ * @typedef {object} RegisterPreset
162
+ * @property {string} label - Display label (e.g., 'Vouvoiement (Formal)', '해요체 (Polite)')
163
+ * @property {string} description - Short description of when to use this preset
164
+ * @property {string} prompt - Full register prompt text injected into LLM prompts
165
+ * @property {string} [deeplFormality] - DeepL API formality value: 'prefer_more', 'prefer_less', or 'default'. Only present on presets for languages where methodSupport.deepl.formality is true.
166
+ */
167
+
168
+ // -----------------------------------------------------------------
169
+ // DiffResult — output of comparing source vs target locale
170
+ // Produced by: diff.js:diffLocale()
171
+ // Consumed by: sync.js
172
+ // -----------------------------------------------------------------
173
+
174
+ /**
175
+ * @typedef {object} DiffResult
176
+ * @property {string[]} missing - Keys in source but not in target
177
+ * @property {string[]} needsTranslation - Keys with [EN] fallback prefix in target
178
+ * @property {string[]} changed - Keys whose source content hash changed since last sync
179
+ * @property {string[]} forced - Keys explicitly requested for re-translation
180
+ * @property {string[]} noTranslate - Exempt keys (config `noTranslate` / auto-detected URL) whose target is not byte-identical to the source and must be copied verbatim. Never translated, gated, or billed.
181
+ * @property {string[]} extra - Keys in target but not in source (stale/orphaned)
182
+ * @property {string[]} toProcess - Deduplicated union of missing + needsTranslation + changed + forced, minus every exempt key
183
+ */
184
+
185
+ // -----------------------------------------------------------------
186
+ // CoachingData — linguistic coaching hints for LLM-coached method
187
+ // Produced by: llm-coached.js:_loadCoachingData()
188
+ // Consumed by: llm-coached.js:buildCoachedSystemMessage/buildCoachedPrompt
189
+ // -----------------------------------------------------------------
190
+
191
+ /**
192
+ * @typedef {object} CoachingData
193
+ * @property {string[]} [grammar_rules] - Grammar rules (e.g., "French adjectives agree in gender/number")
194
+ * @property {Object<string, string>} [dictionary] - Term overrides (e.g., "dashboard" → "tableau de bord")
195
+ * @property {string} [style_notes] - Style guidance (e.g., "Prefer active voice. Avoid anglicisms.")
196
+ */
197
+
198
+ // -----------------------------------------------------------------
199
+ // CLI args shape — parsed CLI arguments passed to command modules
200
+ // Produced by: bin/cli.js (util.parseArgs)
201
+ // Consumed by: lib/commands/*.js
202
+ // -----------------------------------------------------------------
203
+
204
+ /**
205
+ * @typedef {object} CLIArgs
206
+ * @property {string[]} [_] - Positional arguments (command, subcommand, etc.)
207
+ * @property {boolean} [dry] - Preview changes without writing files
208
+ * @property {boolean} [help] - Show help
209
+ * @property {boolean} [version] - Show version
210
+ * @property {boolean} [fallback] - Write [EN]-prefixed placeholders
211
+ * @property {boolean} [yes] - Skip interactive prompts
212
+ * @property {boolean} [undo] - Restore from backup (wrap command)
213
+ * @property {string} [config] - Custom config file path
214
+ * @property {string} [dir] - Override locales directory
215
+ * @property {string} [source] - Override source locale
216
+ * @property {string} [model] - Override translation model
217
+ * @property {string} [method] - Override translation method
218
+ * @property {string} [format] - Locale file format override
219
+ * @property {string} [out] - Output file path (seo sitemap)
220
+ * @property {string} [src] - Source directory for lint/wrap
221
+ * @property {string} ['content-dir'] - Hugo content directory
222
+ * @property {string} ['base-url'] - Site base URL override
223
+ * @property {string} ['min-length'] - Minimum string length to flag
224
+ * @property {string} ['force-keys'] - Comma-separated keys to force re-translate
225
+ * @property {boolean} ['warn-only'] - Exit 0 even with issues
226
+ */
227
+
228
+ // Empty export so modules can import types via:
229
+ // /** @typedef {import('./types.js').PairConfig} PairConfig */
230
+ export {};