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,314 @@
1
+ import { TranslationMethod } from './base.js';
2
+ import { getEnvOrFileVar } from '../api-key.js';
3
+ import { DEFAULT_METHOD_CONCURRENCY } from '../config.js';
4
+ import { estimateProviderCost } from './provider-pricing.js';
5
+ import { extractContentBody } from './content-separator.js';
6
+ import { output } from '../output.js';
7
+ import { fetchWithRetry } from './fetch-with-retry.js';
8
+ import { pMap } from '../concurrent.js';
9
+
10
+ const LIBRETRANSLATE_REQUEST_TIMEOUT_MS = 15000;
11
+
12
+ class LibreTranslateMethod extends TranslationMethod {
13
+ constructor(options = {}) {
14
+ super('libretranslate', options);
15
+ }
16
+
17
+ // ── API resolution helpers ──────────────────────────────────────
18
+
19
+ /**
20
+ * Resolve the LibreTranslate API endpoint URL.
21
+ * Falls back to localhost:5000 for self-hosted instances.
22
+ * @param {object} options - Caller-provided options
23
+ * @returns {string}
24
+ */
25
+ _resolveApiEndpoint(options) {
26
+ return options.libretranslateApiUrl
27
+ || getEnvOrFileVar('LIBRETRANSLATE_API_URL')
28
+ || getEnvOrFileVar('LIBRETRANSLATE_API_URL', options.cwd)
29
+ || 'http://localhost:5000/translate';
30
+ }
31
+
32
+ /**
33
+ * Resolve the LibreTranslate API key (optional for self-hosted).
34
+ * @param {object} options - Caller-provided options
35
+ * @returns {string|null}
36
+ */
37
+ _resolveApiKey(options) {
38
+ return options.libretranslateApiKey
39
+ || getEnvOrFileVar('LIBRETRANSLATE_API_KEY')
40
+ || getEnvOrFileVar('LIBRETRANSLATE_API_KEY', options.cwd);
41
+ }
42
+
43
+ /**
44
+ * Translate a batch of key-value pairs via LibreTranslate API.
45
+ *
46
+ * @param {string[]} keys - Flat dot-notation keys to translate
47
+ * @param {object} sourceFlat - Full flattened source locale
48
+ * @param {import('../types.js').PairConfig} pairConfig - Pair config
49
+ * @param {object} options - { apiKey, batchSize }
50
+ * @returns {object|null} Map of key → translated value, or null
51
+ */
52
+ async translate(keys, sourceFlat, pairConfig, options) {
53
+ const apiEndpoint = this._resolveApiEndpoint(options);
54
+ const apiKey = this._resolveApiKey(options);
55
+
56
+ const targetLocale = pairConfig.target;
57
+ const sourceLocale = pairConfig.source || 'en';
58
+ const allTranslated = {};
59
+
60
+ const maxSegments = pairConfig.batchSize || options.batchSize || 64;
61
+
62
+ const batchChunks = [];
63
+ for (let i = 0; i < keys.length; i += maxSegments) {
64
+ batchChunks.push(keys.slice(i, i + maxSegments));
65
+ }
66
+
67
+ await pMap(batchChunks, async (chunk, idx) => {
68
+ const orderedKeys = [];
69
+ const sourceTexts = [];
70
+
71
+ for (const key of chunk) {
72
+ const val = sourceFlat[key];
73
+ if (val && typeof val === 'string') {
74
+ orderedKeys.push(key);
75
+ sourceTexts.push(val);
76
+ }
77
+ }
78
+
79
+ if (sourceTexts.length === 0) return;
80
+
81
+ const result = await this._translateBatchWithRetry({
82
+ apiEndpoint,
83
+ apiKey,
84
+ orderedKeys,
85
+ sourceTexts,
86
+ sourceLocale,
87
+ targetLocale,
88
+ batchNum: idx + 1,
89
+ });
90
+
91
+ if (result) {
92
+ Object.assign(allTranslated, result);
93
+ }
94
+ }, { concurrency: DEFAULT_METHOD_CONCURRENCY });
95
+
96
+ return Object.keys(allTranslated).length > 0 ? allTranslated : null;
97
+ }
98
+
99
+ /**
100
+ * Translate freeform Markdown content via LibreTranslate API.
101
+ *
102
+ * Same protect/restore approach — ⟦PROTECTED_N⟧ placeholders shield
103
+ * code blocks and shortcodes from the translation engine.
104
+ *
105
+ * @param {string} prompt - Complete translation prompt from buildContentPrompt()
106
+ * @param {import('../types.js').PairConfig} pairConfig - Pair config
107
+ * @param {object} options - {}
108
+ * @returns {string|null} Translated text, or null on failure
109
+ */
110
+ async translateContent(prompt, pairConfig, options) {
111
+ const apiEndpoint = this._resolveApiEndpoint(options);
112
+ const apiKey = this._resolveApiKey(options);
113
+
114
+ // Extract the Markdown body from the translation prompt
115
+ const bodyText = extractContentBody(prompt);
116
+ if (!bodyText.trim()) return null;
117
+
118
+ const targetLocale = pairConfig.target;
119
+ const sourceLocale = pairConfig.source || 'en';
120
+
121
+ const body = {
122
+ q: bodyText,
123
+ source: sourceLocale,
124
+ target: targetLocale,
125
+ format: 'text',
126
+ };
127
+ if (apiKey) {
128
+ body.api_key = apiKey;
129
+ }
130
+
131
+ const response = await fetchWithRetry(apiEndpoint, {
132
+ method: 'POST',
133
+ headers: { 'Content-Type': 'application/json' },
134
+ body: JSON.stringify(body),
135
+ }, {
136
+ label: 'LibreTranslate content',
137
+ timeoutMs: LIBRETRANSLATE_REQUEST_TIMEOUT_MS * 2,
138
+ });
139
+
140
+ if (!response) return null;
141
+
142
+ if (!response.ok) {
143
+ const errorBody = await response.text();
144
+ output.error(`LibreTranslate content: ${response.status} — ${errorBody}`);
145
+ return null;
146
+ }
147
+
148
+ const json = await response.json();
149
+ if (!json?.translatedText) {
150
+ output.error('LibreTranslate content: empty response');
151
+ return null;
152
+ }
153
+
154
+ return json.translatedText;
155
+ }
156
+
157
+ /**
158
+ * Preflight check: verify the LibreTranslate server is reachable.
159
+ *
160
+ * Unlike cloud APIs that just need a key check, LibreTranslate is
161
+ * self-hosted — the server might not be running. We probe it here
162
+ * so the user gets a clear "server is down" message at startup instead
163
+ * of cryptic fetch errors deep in the translation loop.
164
+ */
165
+ async checkReadiness(context) {
166
+ const endpointUrl = this._resolveApiEndpoint(context);
167
+
168
+ // The /translate endpoint base — try to reach the API root
169
+ const baseUrl = endpointUrl.replace(/\/translate\/?$/, '');
170
+
171
+ try {
172
+ const controller = new AbortController();
173
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
174
+
175
+ const response = await fetch(`${baseUrl}/languages`, {
176
+ method: 'GET',
177
+ signal: controller.signal,
178
+ });
179
+ clearTimeout(timeoutId);
180
+
181
+ if (!response.ok) {
182
+ return {
183
+ ready: false,
184
+ reason:
185
+ `LibreTranslate server at ${baseUrl} responded with ${response.status}.\n` +
186
+ ` Check that LibreTranslate is running and accessible.`,
187
+ };
188
+ }
189
+ return { ready: true };
190
+ } catch (err) {
191
+ return {
192
+ ready: false,
193
+ reason:
194
+ `Cannot reach LibreTranslate server at ${baseUrl}.\n` +
195
+ ` ${err.name === 'AbortError' ? 'Connection timed out.' : err.message}\n` +
196
+ ` Start LibreTranslate: docker run -ti --rm -p 5000:5000 libretranslate/libretranslate`,
197
+ };
198
+ }
199
+ }
200
+
201
+ estimateCost(keyCount) {
202
+ return estimateProviderCost('libretranslate', keyCount);
203
+ }
204
+
205
+ getQualityTier() {
206
+ return 'standard';
207
+ }
208
+
209
+ getProvenance() {
210
+ return {
211
+ resources: [
212
+ {
213
+ name: 'LibreTranslate API',
214
+ license: 'AGPL-3.0 (Self-hosted)',
215
+ type: 'api',
216
+ },
217
+ ],
218
+ // AGPL-3.0 — matches shared/method-registry.json (commercialReady:
219
+ // false) and docs/MT_SYSTEMS_UNDER_TEST.md. This said `true` until
220
+ // 2026-08-12 while declaring AGPL in the same object; the CLAUDE.md
221
+ // AGPL boundary makes that a licence-boundary defect, not a nuance.
222
+ // Non-commercial use is unaffected — that lane is not gated.
223
+ commercialReady: false,
224
+ flags: ['COPYLEFT_AGPL'],
225
+ };
226
+ }
227
+
228
+ getSetupHelp() {
229
+ // LibreTranslate is self-hosted — there's no "missing key" case,
230
+ // just a "can't connect" case. Always show the connection guide.
231
+ return [
232
+ '',
233
+ ' ┌─ LibreTranslate Connection Failed ──────────────────────────────┐',
234
+ ' │ Could not connect to LibreTranslate API. │',
235
+ ' │ │',
236
+ ' │ Default endpoint: http://localhost:5000/translate │',
237
+ ' │ │',
238
+ ' │ To use a remote instance: │',
239
+ ' │ export LIBRETRANSLATE_API_URL=https://your-instance/translate │',
240
+ ' │ export LIBRETRANSLATE_API_KEY=... (if required) │',
241
+ ' │ │',
242
+ ' │ Self-host: docker run -p 5000:5000 libretranslate/libretranslate │',
243
+ ' └──────────────────────────────────────────────────────────────────┘',
244
+ ];
245
+ }
246
+
247
+ async _translateBatchWithRetry({
248
+ apiEndpoint,
249
+ apiKey,
250
+ orderedKeys,
251
+ sourceTexts,
252
+ sourceLocale,
253
+ targetLocale,
254
+ batchNum,
255
+ }) {
256
+ const body = {
257
+ q: sourceTexts,
258
+ source: sourceLocale,
259
+ target: targetLocale,
260
+ format: 'text',
261
+ };
262
+
263
+ if (apiKey) {
264
+ body.api_key = apiKey;
265
+ }
266
+
267
+ const response = await fetchWithRetry(apiEndpoint, {
268
+ method: 'POST',
269
+ headers: {
270
+ 'Content-Type': 'application/json',
271
+ },
272
+ body: JSON.stringify(body),
273
+ }, {
274
+ label: `LibreTranslate batch ${batchNum}`,
275
+ timeoutMs: LIBRETRANSLATE_REQUEST_TIMEOUT_MS,
276
+ });
277
+
278
+ if (!response) return null;
279
+
280
+ if (!response.ok) {
281
+ const errorBody = await response.text();
282
+ output.error(`LibreTranslate batch ${batchNum}: ${response.status} — ${errorBody}`);
283
+ return null;
284
+ }
285
+
286
+ const json = await response.json();
287
+ const translatedText = json?.translatedText;
288
+
289
+ if (!translatedText || (Array.isArray(translatedText) && translatedText.length !== orderedKeys.length)) {
290
+ output.error(`LibreTranslate batch ${batchNum}: Response format or length mismatch`);
291
+ return null;
292
+ }
293
+
294
+ const result = {};
295
+ if (Array.isArray(translatedText)) {
296
+ for (let i = 0; i < orderedKeys.length; i++) {
297
+ result[orderedKeys[i]] = translatedText[i];
298
+ }
299
+ } else if (orderedKeys.length === 1 && typeof translatedText === 'string') {
300
+ // If LibreTranslate returned a single string for a single item batch
301
+ result[orderedKeys[0]] = translatedText;
302
+ } else {
303
+ output.error(`LibreTranslate batch ${batchNum}: Unexpected translatedText format`);
304
+ return null;
305
+ }
306
+
307
+ const charCount = sourceTexts.reduce((sum, t) => sum + t.length, 0);
308
+ output.progress(` ✓ LibreTranslate batch ${batchNum} (${orderedKeys.length} keys, ${charCount} chars)`);
309
+
310
+ return result;
311
+ }
312
+ }
313
+
314
+ export { LibreTranslateMethod };