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.
- package/LICENSE +133 -0
- package/README.md +387 -0
- package/bin/cli.js +278 -0
- package/index.js +135 -0
- package/lib/api-key.js +127 -0
- package/lib/autofix.js +432 -0
- package/lib/bridge/method_bridge.py +430 -0
- package/lib/card-source-resolution.mjs +284 -0
- package/lib/cards/cache.js +169 -0
- package/lib/cards/env.js +82 -0
- package/lib/cards/fetch-card-child.js +38 -0
- package/lib/cards/reader.js +435 -0
- package/lib/cards/refresh.js +111 -0
- package/lib/cards/remote.js +387 -0
- package/lib/cldf-export.mjs +540 -0
- package/lib/cldf-terms.mjs +62 -0
- package/lib/command-help.js +790 -0
- package/lib/commands/audit.js +49 -0
- package/lib/commands/card.js +454 -0
- package/lib/commands/doctor.js +559 -0
- package/lib/commands/fonts.js +489 -0
- package/lib/commands/help.js +91 -0
- package/lib/commands/init.js +1259 -0
- package/lib/commands/integrity.js +148 -0
- package/lib/commands/leaderboard.js +478 -0
- package/lib/commands/lint.js +30 -0
- package/lib/commands/models.js +177 -0
- package/lib/commands/plugin.js +103 -0
- package/lib/commands/provenance.js +45 -0
- package/lib/commands/recommend.js +75 -0
- package/lib/commands/register-corpus.js +678 -0
- package/lib/commands/repair-script.js +42 -0
- package/lib/commands/seal-corpus.js +355 -0
- package/lib/commands/seo.js +72 -0
- package/lib/commands/serve.js +147 -0
- package/lib/commands/status.js +265 -0
- package/lib/commands/submit.js +332 -0
- package/lib/commands/sync.js +89 -0
- package/lib/commands/tm.js +573 -0
- package/lib/commands/verify.js +39 -0
- package/lib/commands/watch.js +20 -0
- package/lib/commands/wrap.js +138 -0
- package/lib/commands/xliff.js +327 -0
- package/lib/commercial-eligibility.js +235 -0
- package/lib/concurrent.js +87 -0
- package/lib/config.js +523 -0
- package/lib/contamination-lane.js +76 -0
- package/lib/content-sync.js +731 -0
- package/lib/content.js +733 -0
- package/lib/corpus-registration.mjs +608 -0
- package/lib/cost-report.js +346 -0
- package/lib/diff.js +155 -0
- package/lib/docusaurus-sync.js +1256 -0
- package/lib/flatten.js +55 -0
- package/lib/format.js +954 -0
- package/lib/hash.js +159 -0
- package/lib/icu.js +473 -0
- package/lib/integrity.js +689 -0
- package/lib/license-gate.mjs +478 -0
- package/lib/license-identify.mjs +229 -0
- package/lib/lint.js +629 -0
- package/lib/method-manifest.js +60 -0
- package/lib/methods/anthropic.js +140 -0
- package/lib/methods/apertium.js +163 -0
- package/lib/methods/api.js +316 -0
- package/lib/methods/base.js +184 -0
- package/lib/methods/content-separator.js +45 -0
- package/lib/methods/deepl.js +426 -0
- package/lib/methods/direct-llm.js +586 -0
- package/lib/methods/external.js +332 -0
- package/lib/methods/fetch-with-retry.js +124 -0
- package/lib/methods/gemini.js +147 -0
- package/lib/methods/google-translate.js +402 -0
- package/lib/methods/http-utils.js +122 -0
- package/lib/methods/libretranslate.js +314 -0
- package/lib/methods/llm-coached.js +670 -0
- package/lib/methods/llm.js +592 -0
- package/lib/methods/local.js +76 -0
- package/lib/methods/microsoft-translator.js +331 -0
- package/lib/methods/openai.js +131 -0
- package/lib/methods/openrouter-client.js +327 -0
- package/lib/methods/openrouter-pricing.js +156 -0
- package/lib/methods/provider-env.js +115 -0
- package/lib/methods/provider-pricing.js +310 -0
- package/lib/methods/tilde.js +150 -0
- package/lib/methods/translated.js +229 -0
- package/lib/methods/translation-error.js +80 -0
- package/lib/models.js +258 -0
- package/lib/no-translate.js +233 -0
- package/lib/output.js +238 -0
- package/lib/pairs.js +547 -0
- package/lib/plugins.js +447 -0
- package/lib/provenance.js +323 -0
- package/lib/recommend.js +648 -0
- package/lib/registers.js +1185 -0
- package/lib/repair-script.js +266 -0
- package/lib/scripts.js +994 -0
- package/lib/seal.mjs +464 -0
- package/lib/sealed-qualifier.mjs +211 -0
- package/lib/security.js +59 -0
- package/lib/segment.js +369 -0
- package/lib/seo.js +275 -0
- package/lib/serve.js +854 -0
- package/lib/string-classify.js +85 -0
- package/lib/submit.mjs +344 -0
- package/lib/sync.js +969 -0
- package/lib/tags/bcp47.js +202 -0
- package/lib/tags/resolve.js +314 -0
- package/lib/terminology.js +111 -0
- package/lib/tm-seed.js +294 -0
- package/lib/tm.js +515 -0
- package/lib/translate-pair.js +197 -0
- package/lib/translate.js +203 -0
- package/lib/types.js +230 -0
- package/lib/validate.js +510 -0
- package/lib/verify.js +451 -0
- package/lib/watch.js +145 -0
- package/lib/xliff.js +184 -0
- package/package.json +93 -0
- package/shared/ATTRIBUTION.md +145 -0
- package/shared/CORPORA-CARDS.md +288 -0
- package/shared/DATA-SOVEREIGNTY.md +500 -0
- package/shared/LANGUAGE-CARD-FIELDS.md +532 -0
- package/shared/card-lint-baseline.json +3189 -0
- package/shared/cards-fallback.json +1 -0
- package/shared/catalogue/card-config.json +6091 -0
- package/shared/catalogue/external-results.json +3888 -0
- package/shared/catalogue/gender-guidance.json +1038 -0
- package/shared/catalogue/method-coverage.json +1751 -0
- package/shared/catalogue/metric-coverage.json +170 -0
- package/shared/catalogue/metric-reliability.json +1 -0
- package/shared/catalogue/register-presets.json +3180 -0
- package/shared/catalogue/vitality-scales.json +55 -0
- package/shared/cldr-index.json +1115 -0
- package/shared/code-bridge.json +253 -0
- package/shared/corpora-cards-v1-reference.md +281 -0
- package/shared/curated-dictionary-flags.json +35 -0
- package/shared/curated-endonyms.json +35 -0
- package/shared/curated-fsts.json +51 -0
- package/shared/curated-orthography-conventions.json +26 -0
- package/shared/curated-sil-resources.json +374 -0
- package/shared/curated-tools.json +41 -0
- package/shared/docent/corpus.json +11333 -0
- package/shared/docent/faq.en.json +564 -0
- package/shared/docent/register-blocks.json +60 -0
- package/shared/docent/system-prompt.md +144 -0
- package/shared/domain-taxonomy.json +35 -0
- package/shared/explainers/glossary.json +2975 -0
- package/shared/explainers/tc-features.json +20112 -0
- package/shared/explainers/term-watchlist.json +147 -0
- package/shared/human-services.json +59 -0
- package/shared/license-corrections.json +261 -0
- package/shared/license-evidence.json +13452 -0
- package/shared/licenses.json +6781 -0
- package/shared/method-registry.json +236 -0
- package/shared/metric-registry.json +620 -0
- package/shared/model-aliases.json +7 -0
- package/shared/schemas/champollion-plugin.schema.json +206 -0
- package/shared/schemas/corpora-card.schema.json +957 -0
- package/shared/schemas/domain-taxonomy.schema.json +64 -0
- package/shared/schemas/external-results.schema.json +314 -0
- package/shared/schemas/human-services.schema.json +90 -0
- package/shared/schemas/language-card.schema.json +1308 -0
- package/shared/schemas/licenses.schema.json +155 -0
- package/shared/schemas/method-card.schema.json +412 -0
- package/shared/schemas/method-registry.schema.json +85 -0
- package/shared/schemas/metric-registry.schema.json +96 -0
- package/shared/schemas/metric-reliability.schema.json +178 -0
- package/shared/schemas/model-aliases.schema.json +27 -0
- 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 };
|