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
package/lib/tm-seed.js ADDED
@@ -0,0 +1,294 @@
1
+ /**
2
+ * tm-seed.js — seed the Translation Memory from EXISTING translations.
3
+ *
4
+ * WHY THIS EXISTS:
5
+ * Block-level TM only gains entries as files are (re-)translated. A
6
+ * project that upgraded with a full set of already-translated content
7
+ * files has an empty TM for all of them — so a lost or clobbered
8
+ * .champollion-content.lock re-bills every file through the API even
9
+ * though perfectly good translations sit on disk. `champollion tm seed`
10
+ * walks those existing translations and back-fills the TM: front-matter
11
+ * fields (aligned by field NAME), body blocks (aligned 1:1 by POSITION
12
+ * via segment.js splitBlocks), and the whole body — each keyed with
13
+ * tmMethodKey(pairConfig), exactly as a live sync would store them.
14
+ *
15
+ * SAFETY RULES (never guess alignment):
16
+ * - A (file × locale) is seeded ONLY when the lock manifest entry says
17
+ * the target is up to date (stored hash === SHA-256 of the current
18
+ * source). A stale or missing entry means the target may correspond
19
+ * to an older source — skip.
20
+ * - Body blocks are seeded ONLY when source and target contain the SAME
21
+ * number of translatable blocks. Any mismatch skips the entire file
22
+ * with a warning; a positional guess would poison the TM.
23
+ * - Targets containing the legacy '[EN] ' fallback marker are never
24
+ * seeded — they are untranslated English, not translations.
25
+ *
26
+ * Seeding is additive and idempotent: entries whose cached value already
27
+ * matches the on-disk translation are left untouched; disk is treated as
28
+ * truth when they differ (a hand-polished file wins over an old cache).
29
+ *
30
+ * CACHE-UNIT PARITY (must mirror the sync engines exactly):
31
+ * Block keys/values replicate content-sync/docusaurus-sync block mode —
32
+ * segment the PROTECTED body (protectBlocks → splitBlocks), keep only
33
+ * 'translatable' segments (separators/passthrough are copied verbatim by
34
+ * sync, never cached), and key each on its RESTORED source text with the
35
+ * RESTORED target text as the value. Placeholder numbering is positional
36
+ * per file, so only restored text is stable across files and edits.
37
+ */
38
+
39
+ import fs from 'node:fs';
40
+ import path from 'node:path';
41
+ import crypto from 'node:crypto';
42
+ import { loadTM, saveTM, lookupTM, storeTM, isTMDirty, tmSize, tmMethodKey } from './tm.js';
43
+ import { readContentManifest } from './content-sync.js';
44
+ import { splitBlocks } from './segment.js';
45
+ import { isPathContained } from './security.js';
46
+ import { output } from './output.js';
47
+ import {
48
+ discoverContentFiles,
49
+ getTargetContentPath,
50
+ discoverDocusaurusContentFiles,
51
+ getDocusaurusTargetPath,
52
+ parseContentFile,
53
+ protectBlocks,
54
+ restoreBlocks,
55
+ DEFAULT_TRANSLATABLE_FIELDS,
56
+ } from './content.js';
57
+
58
+ /**
59
+ * Extract the restored source texts of a body's translatable blocks —
60
+ * the exact TM key unit the sync engines use in block mode.
61
+ *
62
+ * @param {string} body - Raw markdown body (front matter stripped)
63
+ * @returns {string[]} Restored translatable block texts, in document order
64
+ */
65
+ function translatableBlockTexts(body) {
66
+ const { protectedBody, blocks } = protectBlocks(body);
67
+ return splitBlocks(protectedBody)
68
+ .filter(seg => seg.type === 'translatable')
69
+ .map(seg => restoreBlocks(seg.text, blocks));
70
+ }
71
+
72
+ /**
73
+ * Seed the TM from existing translated content files.
74
+ *
75
+ * Walks the same (source file × pair) space as the two content sync
76
+ * engines, using the same manifest keys, so what gets seeded is exactly
77
+ * what a future sync would look up:
78
+ * - Hugo lane (contentDir): "relPath:code"
79
+ * - Docusaurus lane: "docusaurus:dirName/relPath:code"
80
+ *
81
+ * @param {object} options
82
+ * @param {string} options.cwd - Project root (lock file + TM location)
83
+ * @param {Map<string, object>} options.pairs - Resolved pair graph (pairKey → pairConfig)
84
+ * @param {string} options.sourceLocale - Source language code
85
+ * @param {string[]|null} [options.translatableFields] - Hugo-lane front matter
86
+ * fields (null → DEFAULT_TRANSLATABLE_FIELDS). The Docusaurus lane always
87
+ * uses the defaults, mirroring runDocusaurusSync.
88
+ * @param {string|null} [options.contentDir] - Hugo content directory (null = lane off)
89
+ * @param {{ localesDir: string, docsDir: string, blogDir: string }|null} [options.docusaurus]
90
+ * Docusaurus lane directories (null = lane off)
91
+ * @param {boolean} [options.dryRun] - Report what would be seeded, write nothing
92
+ * @param {string|null} [options.localeFilter] - Only seed one target locale
93
+ * @returns {{
94
+ * files: Array<{ label: string, code: string, status: 'seeded'|'skipped',
95
+ * reason?: string, fields: number, blocks: number, body: boolean,
96
+ * added: number, existing: number }>,
97
+ * seededFiles: number, skippedFiles: number,
98
+ * entriesAdded: number, entriesExisting: number,
99
+ * tmSizeBefore: number, tmSizeAfter: number,
100
+ * dryRun: boolean, saved: boolean,
101
+ * }}
102
+ */
103
+ function seedTMFromExisting(options) {
104
+ const {
105
+ cwd,
106
+ pairs,
107
+ sourceLocale,
108
+ translatableFields = null,
109
+ contentDir = null,
110
+ docusaurus = null,
111
+ dryRun = false,
112
+ localeFilter = null,
113
+ } = options;
114
+
115
+ const manifest = readContentManifest(cwd);
116
+ const tm = loadTM(cwd);
117
+ const tmSizeBefore = tmSize(tm);
118
+
119
+ const pairEntries = [...pairs.entries()]
120
+ .filter(([, pairConfig]) => !localeFilter || pairConfig.target === localeFilter)
121
+ .sort(([a], [b]) => a.localeCompare(b));
122
+
123
+ // ── Collect candidates from both lanes ─────────────────────────────
124
+ // Each candidate mirrors one (source file × pair) unit of sync work.
125
+ const candidates = [];
126
+
127
+ if (contentDir && fs.existsSync(contentDir)) {
128
+ const fieldsList = translatableFields || DEFAULT_TRANSLATABLE_FIELDS;
129
+ for (const sourcePath of discoverContentFiles(contentDir, sourceLocale)) {
130
+ const relPath = path.relative(contentDir, sourcePath);
131
+ for (const [, pairConfig] of pairEntries) {
132
+ const code = pairConfig.target;
133
+ const targetPath = getTargetContentPath(sourcePath, code, sourceLocale);
134
+ if (!isPathContained(targetPath, contentDir)) continue;
135
+ candidates.push({
136
+ sourcePath, targetPath, pairConfig, code, fieldsList,
137
+ label: relPath,
138
+ manifestKey: `${relPath}:${code}`,
139
+ });
140
+ }
141
+ }
142
+ }
143
+
144
+ if (docusaurus) {
145
+ const { localesDir, docsDir, blogDir } = docusaurus;
146
+ const contentSources = [];
147
+ if (docsDir && fs.existsSync(docsDir)) {
148
+ contentSources.push({ dir: docsDir, plugin: 'docusaurus-plugin-content-docs' });
149
+ }
150
+ if (blogDir && fs.existsSync(blogDir)) {
151
+ contentSources.push({ dir: blogDir, plugin: 'docusaurus-plugin-content-blog' });
152
+ }
153
+ for (const { dir, plugin } of contentSources) {
154
+ const dirName = path.basename(dir);
155
+ for (const sourcePath of discoverDocusaurusContentFiles(dir)) {
156
+ const relPath = path.relative(dir, sourcePath);
157
+ for (const [, pairConfig] of pairEntries) {
158
+ const code = pairConfig.target;
159
+ const targetPath = getDocusaurusTargetPath(sourcePath, dir, code, localesDir, plugin);
160
+ if (!isPathContained(targetPath, localesDir)) continue;
161
+ candidates.push({
162
+ sourcePath, targetPath, pairConfig, code,
163
+ fieldsList: DEFAULT_TRANSLATABLE_FIELDS,
164
+ label: `${dirName}/${relPath}`,
165
+ manifestKey: `docusaurus:${dirName}/${relPath}:${code}`,
166
+ });
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ // ── Seed each candidate behind the safety gates ─────────────────────
173
+ const files = [];
174
+ let seededFiles = 0;
175
+ let skippedFiles = 0;
176
+ let entriesAdded = 0;
177
+ let entriesExisting = 0;
178
+
179
+ const sourceCache = new Map(); // sourcePath → { raw, hash }
180
+
181
+ for (const cand of candidates) {
182
+ const record = {
183
+ label: cand.label, code: cand.code, status: 'skipped',
184
+ fields: 0, blocks: 0, body: false, added: 0, existing: 0,
185
+ };
186
+ files.push(record);
187
+
188
+ if (!fs.existsSync(cand.targetPath)) {
189
+ record.reason = 'no translated file';
190
+ skippedFiles++;
191
+ continue;
192
+ }
193
+
194
+ if (!sourceCache.has(cand.sourcePath)) {
195
+ const raw = fs.readFileSync(cand.sourcePath, 'utf-8');
196
+ const hash = crypto.createHash('sha256').update(raw, 'utf-8').digest('hex');
197
+ sourceCache.set(cand.sourcePath, { raw, hash });
198
+ }
199
+ const { raw, hash } = sourceCache.get(cand.sourcePath);
200
+
201
+ // Lock-manifest gate: only an entry that matches the CURRENT source
202
+ // hash proves the on-disk translation corresponds to this source.
203
+ const storedHash = cand.manifestKey in manifest ? manifest[cand.manifestKey] : null;
204
+ if (storedHash === null) {
205
+ record.reason = 'no lock entry — cannot prove the translation matches this source';
206
+ skippedFiles++;
207
+ continue;
208
+ }
209
+ if (storedHash !== hash) {
210
+ record.reason = 'lock entry is stale (source changed since last sync)';
211
+ skippedFiles++;
212
+ continue;
213
+ }
214
+
215
+ const targetRaw = fs.readFileSync(cand.targetPath, 'utf-8');
216
+ if (targetRaw.includes('[EN] ')) {
217
+ record.reason = 'target is a legacy [EN] fallback, not a translation';
218
+ skippedFiles++;
219
+ continue;
220
+ }
221
+
222
+ const src = parseContentFile(raw);
223
+ const tgt = parseContentFile(targetRaw);
224
+ const srcBlocks = translatableBlockTexts(src.body);
225
+ const tgtBlocks = translatableBlockTexts(tgt.body);
226
+
227
+ // Positional alignment is only trustworthy when the counts agree —
228
+ // skip the WHOLE file otherwise, never seed a guessed pairing.
229
+ if (srcBlocks.length !== tgtBlocks.length) {
230
+ record.reason = `translatable block count mismatch (source ${srcBlocks.length} vs target ${tgtBlocks.length})`;
231
+ skippedFiles++;
232
+ output.warn(`tm seed: ${cand.label} → ${cand.code} skipped — ${record.reason}`);
233
+ continue;
234
+ }
235
+
236
+ const tmKey = tmMethodKey(cand.pairConfig);
237
+ const seedEntry = (source, translation) => {
238
+ if (typeof source !== 'string' || source.trim() === '') return false;
239
+ if (typeof translation !== 'string' || translation.trim() === '') return false;
240
+ if (lookupTM(tm, source, cand.code, tmKey) === translation) {
241
+ record.existing++;
242
+ entriesExisting++;
243
+ } else {
244
+ if (!dryRun) storeTM(tm, source, cand.code, tmKey, translation);
245
+ record.added++;
246
+ entriesAdded++;
247
+ }
248
+ return true;
249
+ };
250
+
251
+ // Front-matter fields — aligned by NAME, the same per-field cache unit
252
+ // the sync path looks up via partitionByTM.
253
+ if (src.hasFrontMatter && tgt.hasFrontMatter) {
254
+ for (const field of cand.fieldsList) {
255
+ if (typeof src.frontMatter[field] === 'string' && typeof tgt.frontMatter[field] === 'string') {
256
+ if (seedEntry(src.frontMatter[field], tgt.frontMatter[field])) record.fields++;
257
+ }
258
+ }
259
+ }
260
+
261
+ // Body blocks — aligned 1:1 by position (counts verified equal above).
262
+ for (let i = 0; i < srcBlocks.length; i++) {
263
+ if (seedEntry(srcBlocks[i], tgtBlocks[i])) record.blocks++;
264
+ }
265
+
266
+ // Whole body — the cache unit the current sync path serves bodies from.
267
+ if (src.body.trim() && tgt.body.trim()) {
268
+ record.body = seedEntry(src.body, tgt.body);
269
+ }
270
+
271
+ record.status = 'seeded';
272
+ seededFiles++;
273
+ }
274
+
275
+ let saved = false;
276
+ if (!dryRun && isTMDirty(tm)) {
277
+ saveTM(cwd, tm);
278
+ saved = true;
279
+ }
280
+
281
+ return {
282
+ files,
283
+ seededFiles,
284
+ skippedFiles,
285
+ entriesAdded,
286
+ entriesExisting,
287
+ tmSizeBefore,
288
+ tmSizeAfter: tmSize(tm),
289
+ dryRun,
290
+ saved,
291
+ };
292
+ }
293
+
294
+ export { seedTMFromExisting };