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,489 @@
1
+ /**
2
+ * Command: fonts
3
+ *
4
+ * Manages PUA web fonts for script converters that require them.
5
+ *
6
+ * Subcommands:
7
+ * list — Show which fonts are needed by configured languages
8
+ * install — Download PUA fonts into the project's static assets directory
9
+ *
10
+ * HOW IT WORKS:
11
+ * 1. Reads the project config to find target languages
12
+ * 2. For each language, checks getConverterInfo() for a fontNote
13
+ * 3. fontNote presence means that language's script converter outputs
14
+ * PUA characters that require a custom web font to render
15
+ * 4. Downloads the font from its source repository and writes it to
16
+ * a local directory (auto-detected or user-specified via --dir)
17
+ *
18
+ * WHY this exists:
19
+ * champollion does NOT bundle PUA fonts — they add binary weight to the
20
+ * npm package and have varied licenses (SIL OFL, GPL). This command
21
+ * lets users opt-in to downloading only the fonts they need, with
22
+ * clear license attribution at install time.
23
+ */
24
+
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import { execSync } from 'node:child_process';
28
+ import { getConverterInfo, SCRIPT_CONVERTERS, resolveTargetScript } from '../scripts.js';
29
+ import { getLanguageCard } from '../registers.js';
30
+ import { resolveConfig, detectDocusaurus } from '../config.js';
31
+ import { output } from '../output.js';
32
+
33
+ // ── Font Registry ────────────────────────────────────────────────
34
+ //
35
+ // Each entry maps a locale code to its font metadata.
36
+ // Source URLs point to open-source repositories with verified licenses.
37
+ // Both pIqaD and Tengwar are distributed as ZIP archives containing TTFs.
38
+
39
+ const FONT_REGISTRY = {
40
+ tlh: {
41
+ family: 'pIqaD',
42
+ displayName: 'pIqaD qolqoS',
43
+ filename: 'pIqaDqolqoS.ttf',
44
+ // The compiled TTF is only in the release ZIP, not in the repo tree
45
+ source: 'https://github.com/dadap/pIqaD-fonts/releases/download/pIqaD-fonts-r1/pIqaD-fonts.zip',
46
+ archiveType: 'zip',
47
+ archiveInnerPath: 'pIqaD-fonts/pIqaD-qolqoS.ttf',
48
+ license: 'SIL Open Font License 1.1',
49
+ licenseUrl: 'https://github.com/dadap/pIqaD-fonts/blob/master/LICENSE',
50
+ unicodeRange: 'U+F8D0-F8FF',
51
+ cssSelector: ':lang(tlh), [data-script="piqad"]',
52
+ scriptLabel: 'Klingon pIqaD',
53
+ },
54
+ 'x-elvish-s': {
55
+ family: 'Tengwar',
56
+ displayName: 'FreeMonoTengwar',
57
+ filename: 'FreeMonoTengwar.ttf',
58
+ // SourceForge "latest download" redirects to the ZIP
59
+ source: 'https://sourceforge.net/projects/freetengwar/files/latest/download',
60
+ archiveType: 'zip',
61
+ archiveInnerPath: 'FreeMonoTengwar.2013-07-21/FreeMonoTengwar.ttf',
62
+ license: 'GNU GPL v3 (with font exception)',
63
+ licenseUrl: 'https://sourceforge.net/projects/freetengwar/',
64
+ unicodeRange: 'U+E000-E07F',
65
+ cssSelector: ':lang(x-elvish-s), [data-script="tengwar"]',
66
+ scriptLabel: 'Tolkien Tengwar',
67
+ },
68
+ 'x-kryptonian': {
69
+ family: 'Kryptonian',
70
+ displayName: '(user-provided)',
71
+ filename: 'kryptonian.ttf',
72
+ source: null, // No open-source PUA font available
73
+ sourceNote: 'No open-source PUA Kryptonian font exists. See kryptonian.info or create one with FontForge.',
74
+ license: 'Varies — must be sourced by user',
75
+ licenseUrl: null,
76
+ unicodeRange: 'U+E100-E119',
77
+ cssSelector: ':lang(x-kryptonian), [data-script="kryptonian"]',
78
+ scriptLabel: 'Kryptonian',
79
+ },
80
+ };
81
+
82
+ /**
83
+ * Detect the best output directory for fonts based on project structure.
84
+ *
85
+ * Priority:
86
+ * 1. --dir flag (user override)
87
+ * 2. Docusaurus project → static/fonts/ (or website/static/fonts/)
88
+ * 3. Hugo project → static/fonts/
89
+ * 4. Default → public/fonts/
90
+ */
91
+ function detectFontsDir(cwd, args) {
92
+ if (args.dir) {
93
+ return path.resolve(cwd, args.dir);
94
+ }
95
+
96
+ // Check for Docusaurus in cwd
97
+ if (detectDocusaurus(cwd)) {
98
+ return path.join(cwd, 'static', 'fonts');
99
+ }
100
+
101
+ // Check for Docusaurus in a website/ subdirectory
102
+ const websiteDir = path.join(cwd, 'website');
103
+ if (fs.existsSync(websiteDir) && detectDocusaurus(websiteDir)) {
104
+ return path.join(websiteDir, 'static', 'fonts');
105
+ }
106
+
107
+ // Check for Hugo (config.toml or hugo.toml)
108
+ const hugoConfigs = ['config.toml', 'hugo.toml', 'config.yaml', 'hugo.yaml'];
109
+ if (hugoConfigs.some(f => fs.existsSync(path.join(cwd, f)))) {
110
+ return path.join(cwd, 'static', 'fonts');
111
+ }
112
+
113
+ // Default: public/fonts/ (React, Next.js, Vite)
114
+ return path.join(cwd, 'public', 'fonts');
115
+ }
116
+
117
+ /**
118
+ * Get locale codes from the project config that need PUA fonts.
119
+ */
120
+ function getNeededFonts(config) {
121
+ const needed = [];
122
+ const declined = [];
123
+
124
+ let codes = [];
125
+ if (Array.isArray(config.languages)) {
126
+ codes = config.languages;
127
+ } else if (config.languages && typeof config.languages === 'object') {
128
+ codes = Object.keys(config.languages);
129
+ }
130
+
131
+ for (const code of codes) {
132
+ const info = getConverterInfo(code);
133
+ if (!info || !info.fontNote) continue;
134
+ const font = FONT_REGISTRY[code];
135
+ if (!font) continue;
136
+
137
+ // A font is only NEEDED when this project's script resolution actually
138
+ // emits the PUA script. A declined (default romanization) locale is
139
+ // listed separately so the user learns the opt-in exists — but is not
140
+ // told to download a font nothing will use.
141
+ let converts = false;
142
+ try {
143
+ const langConfig = config.resolvedLanguages?.[code] || {};
144
+ const resolution = resolveTargetScript(code, langConfig, getLanguageCard(code));
145
+ converts = !!resolution.converterKey;
146
+ } catch {
147
+ // Invalid script config — sync will fail loud; fonts stays quiet.
148
+ }
149
+
150
+ if (converts) needed.push({ code, info, font });
151
+ else declined.push({ code, info, font });
152
+ }
153
+
154
+ return Object.assign(needed, { declined });
155
+ }
156
+
157
+ /**
158
+ * Get ALL registered PUA fonts (for listing when no config exists).
159
+ */
160
+ function getAllPuaFonts() {
161
+ const fonts = [];
162
+ for (const [code] of Object.entries(SCRIPT_CONVERTERS)) {
163
+ const info = getConverterInfo(code);
164
+ if (info && info.fontNote) {
165
+ const font = FONT_REGISTRY[code];
166
+ if (font) {
167
+ fonts.push({ code, info, font });
168
+ }
169
+ }
170
+ }
171
+ return fonts;
172
+ }
173
+
174
+ /**
175
+ * Download a file from a URL using Node.js built-in fetch.
176
+ */
177
+ async function downloadFile(url, destPath) {
178
+ try {
179
+ const response = await fetch(url, { redirect: 'follow' });
180
+ if (!response.ok) {
181
+ output.error(` HTTP ${response.status} from ${url}`);
182
+ return false;
183
+ }
184
+
185
+ const buffer = Buffer.from(await response.arrayBuffer());
186
+
187
+ if (buffer.length < 512) {
188
+ output.error(` Downloaded file is too small (${buffer.length} bytes).`);
189
+ return false;
190
+ }
191
+
192
+ fs.writeFileSync(destPath, buffer);
193
+ return true;
194
+ } catch (err) {
195
+ output.error(` Download failed: ${err.message}`);
196
+ return false;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Extract a single file from a ZIP archive using the system `unzip` command.
202
+ * Falls back to manual instructions if `unzip` isn't available.
203
+ *
204
+ * @param {string} zipPath - Path to the ZIP file
205
+ * @param {string} innerPath - Path inside the ZIP (e.g., "pIqaD-fonts/pIqaD-qolqoS.ttf")
206
+ * @param {string} destPath - Where to write the extracted file
207
+ * @returns {boolean} true on success
208
+ */
209
+ function extractFromZip(zipPath, innerPath, destPath) {
210
+ const destDir = path.dirname(destPath);
211
+ const destFilename = path.basename(destPath);
212
+ const extractedName = path.basename(innerPath);
213
+
214
+ try {
215
+ // -j: junk paths (flatten directory structure)
216
+ // -o: overwrite without prompting
217
+ execSync(`unzip -j -o "${zipPath}" "${innerPath}" -d "${destDir}"`, {
218
+ stdio: 'pipe',
219
+ });
220
+
221
+ // Rename if the extracted filename differs from our target
222
+ const extractedPath = path.join(destDir, extractedName);
223
+ if (extractedName !== destFilename && fs.existsSync(extractedPath)) {
224
+ fs.renameSync(extractedPath, destPath);
225
+ }
226
+
227
+ return fs.existsSync(destPath);
228
+ } catch {
229
+ output.error(` Could not extract automatically (unzip not found or failed).`);
230
+ output.raw(` Manual steps:`);
231
+ output.raw(` unzip "${zipPath}" "${innerPath}" -d "${destDir}"`);
232
+ output.raw(` mv "${path.join(destDir, extractedName)}" "${destPath}"`);
233
+ return false;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Download and install a single font.
239
+ * Handles both direct TTF downloads and ZIP archive extraction.
240
+ */
241
+ async function installFont(font, destPath) {
242
+ const fontsDir = path.dirname(destPath);
243
+
244
+ if (font.archiveType === 'zip') {
245
+ // Download ZIP to temp, extract the TTF, clean up
246
+ const tempZip = path.join(fontsDir, `_temp_${font.filename}.zip`);
247
+ const ok = await downloadFile(font.source, tempZip);
248
+ if (!ok) return false;
249
+
250
+ const extracted = extractFromZip(tempZip, font.archiveInnerPath, destPath);
251
+
252
+ // Clean up temp ZIP regardless of extraction result
253
+ if (fs.existsSync(tempZip)) {
254
+ fs.unlinkSync(tempZip);
255
+ }
256
+
257
+ return extracted;
258
+ }
259
+
260
+ // Direct download (no ZIP)
261
+ return await downloadFile(font.source, destPath);
262
+ }
263
+
264
+ /**
265
+ * Generate a CSS snippet with @font-face declarations for installed fonts.
266
+ */
267
+ function generateCssSnippet(fonts) {
268
+ const lines = [
269
+ '/* PUA Conlang Fonts — generated by `champollion fonts install --css` */',
270
+ '/* Add this to your stylesheet or import it. */',
271
+ '',
272
+ ];
273
+
274
+ for (const { font } of fonts) {
275
+ lines.push(`@font-face {`);
276
+ lines.push(` font-family: '${font.family}';`);
277
+ lines.push(` src: url('/fonts/${font.filename}') format('truetype');`);
278
+ lines.push(` font-display: swap;`);
279
+ lines.push(` unicode-range: ${font.unicodeRange};`);
280
+ lines.push(`}`);
281
+ lines.push('');
282
+ lines.push(`${font.cssSelector} {`);
283
+ lines.push(` font-family: '${font.family}', sans-serif;`);
284
+ lines.push(`}`);
285
+ lines.push('');
286
+ }
287
+
288
+ return lines.join('\n');
289
+ }
290
+
291
+ // ── Subcommands ──────────────────────────────────────────────────
292
+
293
+ /**
294
+ * `champollion fonts list` — show which fonts are needed.
295
+ */
296
+ async function subList(args, cwd) {
297
+ let fonts;
298
+ let fromConfig = false;
299
+
300
+ try {
301
+ const config = resolveConfig({ config: args.config }, cwd);
302
+ fonts = getNeededFonts(config);
303
+ fromConfig = true;
304
+ } catch {
305
+ fonts = getAllPuaFonts();
306
+ }
307
+
308
+ if (fonts.length === 0) {
309
+ if (fromConfig) {
310
+ output.raw('\n No configured languages require PUA web fonts.');
311
+ output.raw(' Script converters for crk (Cree) and sr (Serbian) use native Unicode.');
312
+ for (const { code, info } of fonts.declined || []) {
313
+ output.raw(` ${code}: script conversion not enabled (writes ${info.from}) — set "script": "${info.toScript || code}" to emit ${info.to}, which needs a font.`);
314
+ }
315
+ output.raw('');
316
+ } else {
317
+ output.raw('\n No PUA fonts registered.\n');
318
+ }
319
+ return 0;
320
+ }
321
+
322
+ const fontsDir = detectFontsDir(cwd, args);
323
+
324
+ output.raw('');
325
+ output.raw(fromConfig
326
+ ? ` PUA Fonts Needed (from config):\n`
327
+ : ` All PUA Font-Requiring Scripts:\n`
328
+ );
329
+
330
+ for (const { code, font } of fonts) {
331
+ const installed = fs.existsSync(path.join(fontsDir, font.filename));
332
+ const status = installed ? '✅ installed' : '⬜ not installed';
333
+ const source = font.source ? 'downloadable' : 'user-provided';
334
+
335
+ output.raw(` ${code.padEnd(14)} ${font.scriptLabel}`);
336
+ output.raw(` Font: ${font.displayName} [${font.license}]`);
337
+ output.raw(` PUA: ${font.unicodeRange} | ${status} | ${source}`);
338
+ output.raw('');
339
+ }
340
+
341
+ for (const { code, info } of fonts.declined || []) {
342
+ output.raw(` ${code.padEnd(14)} conversion not enabled — writes ${info.from}, no font required.`);
343
+ output.raw(` Set "script": "${info.toScript || code}" to emit ${info.to}.`);
344
+ output.raw('');
345
+ }
346
+
347
+ output.raw(` Font directory: ${fontsDir}`);
348
+ output.raw(` Run \`champollion fonts install\` to download available fonts.\n`);
349
+
350
+ return 0;
351
+ }
352
+
353
+ /**
354
+ * `champollion fonts install` — download fonts into the project.
355
+ */
356
+ async function subInstall(args, cwd) {
357
+ let fonts;
358
+
359
+ try {
360
+ const config = resolveConfig({ config: args.config }, cwd);
361
+ fonts = getNeededFonts(config);
362
+ } catch {
363
+ output.raw(' No project config found — installing all available PUA fonts.\n');
364
+ fonts = getAllPuaFonts();
365
+ }
366
+
367
+ if (fonts.length === 0) {
368
+ output.raw('\n No configured languages require PUA web fonts.\n');
369
+ return 0;
370
+ }
371
+
372
+ const fontsDir = detectFontsDir(cwd, args);
373
+
374
+ // Ensure output directory exists
375
+ if (!fs.existsSync(fontsDir)) {
376
+ fs.mkdirSync(fontsDir, { recursive: true });
377
+ output.raw(` Created: ${fontsDir}`);
378
+ }
379
+
380
+ output.raw('');
381
+ let installed = 0;
382
+ let skipped = 0;
383
+ const installedFonts = [];
384
+
385
+ for (const entry of fonts) {
386
+ const { font } = entry;
387
+ const destPath = path.join(fontsDir, font.filename);
388
+
389
+ // Skip if already installed
390
+ if (fs.existsSync(destPath)) {
391
+ output.raw(` ✅ ${font.scriptLabel} — already installed (${font.filename})`);
392
+ installedFonts.push(entry);
393
+ skipped++;
394
+ continue;
395
+ }
396
+
397
+ // Skip if no source URL (user must provide manually)
398
+ if (!font.source) {
399
+ output.raw(` ⬜ ${font.scriptLabel} — no auto-download available`);
400
+ if (font.sourceNote) output.raw(` ${font.sourceNote}`);
401
+ output.raw(` Place your font file at: ${destPath}`);
402
+ output.raw('');
403
+ continue;
404
+ }
405
+
406
+ // Download and install (handles both direct downloads and ZIP extraction)
407
+ output.raw(` ⬇ ${font.scriptLabel} — downloading...`);
408
+ const ok = await installFont(font, destPath);
409
+ if (ok) {
410
+ const size = fs.statSync(destPath).size;
411
+ const sizeKB = (size / 1024).toFixed(0);
412
+ output.raw(` ✅ ${font.scriptLabel} — installed (${font.filename}, ${sizeKB}KB)`);
413
+ output.raw(` License: ${font.license}`);
414
+ installedFonts.push(entry);
415
+ installed++;
416
+ }
417
+ }
418
+
419
+ output.raw('');
420
+ output.raw(` ${installed} font(s) installed, ${skipped} already present.`);
421
+ output.raw(` Location: ${fontsDir}`);
422
+
423
+ // Generate CSS snippet if requested
424
+ if (args.css && installedFonts.length > 0) {
425
+ const css = generateCssSnippet(installedFonts);
426
+ const cssPath = path.join(fontsDir, 'conlang-fonts.css');
427
+ fs.writeFileSync(cssPath, css, 'utf-8');
428
+ output.raw(` CSS snippet: ${cssPath}`);
429
+ }
430
+
431
+ // Show usage guidance
432
+ if (installedFonts.length > 0) {
433
+ output.raw('');
434
+ output.raw(' Add to your CSS:');
435
+ output.raw('');
436
+ for (const { font } of installedFonts) {
437
+ output.raw(` @font-face {`);
438
+ output.raw(` font-family: '${font.family}';`);
439
+ output.raw(` src: url('/fonts/${font.filename}') format('truetype');`);
440
+ output.raw(` unicode-range: ${font.unicodeRange};`);
441
+ output.raw(` }`);
442
+ output.raw('');
443
+ }
444
+ }
445
+
446
+ return 0;
447
+ }
448
+
449
+ // ── Main Entry Point ─────────────────────────────────────────────
450
+
451
+ /**
452
+ * @param {import('../types.js').CLIArgs} args - Parsed CLI arguments
453
+ * @param {string} cwd - Working directory
454
+ * @returns {Promise<number>} Exit code (0 = success, 1 = error)
455
+ */
456
+ async function run(args, cwd) {
457
+ const subcommand = args._[1];
458
+
459
+ if (subcommand === 'list') {
460
+ return await subList(args, cwd);
461
+ }
462
+
463
+ if (subcommand === 'install') {
464
+ return await subInstall(args, cwd);
465
+ }
466
+
467
+ // No valid subcommand — show help
468
+ output.raw(`
469
+ Font Commands:
470
+
471
+ champollion fonts list Show which PUA fonts are needed
472
+ champollion fonts install Download fonts for configured languages
473
+ champollion fonts install --dir . Override output directory
474
+ champollion fonts install --css Also generate a CSS snippet file
475
+
476
+ PUA (Private Use Area) fonts are needed for script converters that
477
+ output characters outside standard Unicode:
478
+
479
+ tlh Klingon pIqaD (U+F8D0–F8FF)
480
+ x-elvish-s Tolkien Tengwar (U+E000–E07F)
481
+ x-kryptonian Kryptonian (U+E100–E119)
482
+
483
+ Native Unicode converters (crk → Cree Syllabics, sr → Cyrillic)
484
+ do NOT require font installation.
485
+ `);
486
+ return 0;
487
+ }
488
+
489
+ export { run, FONT_REGISTRY };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Command: help
3
+ *
4
+ * Prints the full CLI help screen with all commands, options,
5
+ * supported formats, and quick-start instructions.
6
+ */
7
+
8
+ import { CONFIG_FILENAMES, DEFAULT_OPENROUTER_MODEL } from '../config.js';
9
+
10
+ const DEFAULT_CONFIG_FILENAME = CONFIG_FILENAMES[0];
11
+
12
+ /**
13
+ * @returns {void}
14
+ */
15
+ function run() {
16
+ console.log(`
17
+ champollion — Research-grade translation engine for i18n projects
18
+
19
+ COMMANDS
20
+ init Interactive setup wizard (or --yes for quick defaults)
21
+ sync Translate & sync all locale files
22
+ serve Serve this project's translation stack over HTTP (api-method contract)
23
+ watch Auto-sync when the source file changes
24
+ audit List all untranslated [EN] fallback values
25
+ card Pretty-print a language card (card \<code\> [--json])
26
+ register-corpus Register a corpus: pick a license + exposure tier (local-only/private/public/sealed)
27
+ seal-corpus Sealed-tier crypto verbs: keygen / seal / open (organizer-node bridge)
28
+ submit Propose an index entry (review-gated): print a pre-filled GitHub issue
29
+ lint Scan source files for hardcoded strings (pre-commit gate)
30
+ wrap Auto-wrap hardcoded strings in t() calls (with undo)
31
+ seo Generate hreflang, sitemap.xml, or JSON-LD schema
32
+ integrity Audit locale files for placeholder/encoding issues
33
+ repair-script Restore romanization where script conversion was unwanted
34
+ status Show pair graph, methods, and config summary
35
+ provenance Show licensing & resource dependencies for all pairs
36
+ plugin Manage method plugins (install, remove, list)
37
+ fonts Download web fonts for PUA script converters
38
+ tm Manage Translation Memory cache (stats, clear, seed, prune)
39
+ xliff Export/import XLIFF 1.2 for professional review
40
+ models List available models for a provider
41
+ verify Verify translations are present and correct (CI gate)
42
+ leaderboard Show MT leaderboard from Supabase (--pair, --sort, --json)
43
+ recommend Method guidance for a pair — availability + cited evidence (--use, --json)
44
+ doctor System health check (cards, config, FSTs, methods)
45
+
46
+ OPTIONS
47
+ --config <path> Path to config file (default: ${DEFAULT_CONFIG_FILENAME})
48
+ --dir <path> Override locales directory
49
+ --content-dir <p> Hugo content directory for Markdown translation
50
+ --source <code> Override source locale (default: en)
51
+ --base-url <url> Override base URL for SEO commands
52
+ --model <model> Override translation model
53
+ --method <method> Default translation method: llm, llm-coached, google-translate, api, deepl, microsoft-translator, libretranslate, openai, anthropic, gemini
54
+ --format <fmt> Locale file format: json, toml, yaml, or auto (default: auto)
55
+ --dry Preview changes without writing files
56
+ --force-keys <k> Comma-separated dot-notation keys to force re-translate
57
+ --src <path> Source directory for lint/wrap (auto-detected)
58
+ --min-length <n> Minimum string length to flag (default: 2)
59
+ --warn-only Exit 0 even with issues (lint, integrity)
60
+ --undo Restore files from .champollion-backup/ (wrap)
61
+ --out <path> Write output to file (seo sitemap, xliff export)
62
+ --locale <code> Target locale (xliff export, tm clear, tm seed)
63
+ --no-tm Skip Translation Memory cache for this sync run
64
+ --no-verify Skip post-sync verification pass
65
+
66
+ SUPPORTED FORMATS
67
+ json Standard JSON (next-intl, i18next, react-intl)
68
+ toml Hugo i18n TOML files (i18n/*.toml)
69
+ yaml Hugo i18n YAML files (i18n/*.yaml)
70
+ auto Auto-detect from file extensions in locales directory
71
+
72
+ QUICK START
73
+ 1. Set OPENROUTER_API_KEY (or provider key like OPENAI_API_KEY, DEEPL_API_KEY, etc.) in your environment or .env.local
74
+ 2. Put your source locale (en.json / en.toml / en.yaml) in a locales/ directory
75
+ 3. Run: champollion sync
76
+
77
+ The tool will:
78
+ • Auto-detect locale file format (JSON, TOML, or YAML)
79
+ • Translate missing keys via OpenRouter (${DEFAULT_OPENROUTER_MODEL})
80
+ • Translate Hugo Markdown content files (if --content-dir is set)
81
+ • Fail loud on any translation errors (no silent failures)
82
+ • Verify translations after writing (key parity, placeholders, script compliance)
83
+ • Batch translations to avoid token overflow
84
+ • Preserve your file structure and formatting
85
+ • Cache translations in Translation Memory to avoid redundant API calls
86
+
87
+ Run champollion <command> --help for detailed help on any command.
88
+ `);
89
+ }
90
+
91
+ export { run };