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,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: audit
|
|
3
|
+
*
|
|
4
|
+
* Lists untranslated [EN] fallback values across all locale files.
|
|
5
|
+
* Returns exit code 1 if any untranslated keys exist — usable as a CI gate.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { runSync } from '../sync.js';
|
|
9
|
+
import { output } from '../output.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {import('../types.js').CLIArgs} args - Parsed CLI arguments
|
|
13
|
+
* @param {string} cwd - Working directory
|
|
14
|
+
* @returns {Promise<number>} Exit code (0 = success, 1 = error)
|
|
15
|
+
*/
|
|
16
|
+
async function run(args, cwd) {
|
|
17
|
+
// --json: machine-readable NDJSON (same controller sync uses) — the audit
|
|
18
|
+
// branch in lib/sync.js emits a structured summary with the untranslated
|
|
19
|
+
// key list per locale. --quiet: warnings and errors only.
|
|
20
|
+
if (args.json) output.setMode('json');
|
|
21
|
+
else if (args.quiet) output.setMode('quiet');
|
|
22
|
+
|
|
23
|
+
let result;
|
|
24
|
+
try {
|
|
25
|
+
result = await runSync({
|
|
26
|
+
audit: true,
|
|
27
|
+
cwd,
|
|
28
|
+
cliArgs: args,
|
|
29
|
+
});
|
|
30
|
+
} catch (err) {
|
|
31
|
+
// Keep --json stdout machine-parseable even on a hard error (missing
|
|
32
|
+
// locales dir, bad config) — mirror commands/sync.js.
|
|
33
|
+
if (args.json) {
|
|
34
|
+
output.summary({ command: 'audit', ok: false, error: err.message });
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Exit 1 if any keys still need translation OR a configured locale file
|
|
41
|
+
// is missing entirely (parity gate). A missing file means zero translation
|
|
42
|
+
// done for that locale — a CI gate must never go green on it.
|
|
43
|
+
if (result && (result.untranslatedCount > 0 || result.missingLocaleCount > 0)) {
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { run };
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: card
|
|
3
|
+
*
|
|
4
|
+
* Pretty-prints a language card from shared/language-cards/.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* champollion card crk — Show Plains Cree card
|
|
8
|
+
* champollion card spa — Show Spanish card
|
|
9
|
+
* champollion card crk --json — Output raw JSON
|
|
10
|
+
*
|
|
11
|
+
* WHY: Language cards are deeply nested JSON files with 50+ fields.
|
|
12
|
+
* Developers and contributors need a quick way to inspect a card's
|
|
13
|
+
* key properties without scrolling through raw JSON. This command
|
|
14
|
+
* formats the card into logical sections with ANSI colors for
|
|
15
|
+
* scannability.
|
|
16
|
+
*
|
|
17
|
+
* Exit codes:
|
|
18
|
+
* 0 — Card displayed successfully
|
|
19
|
+
* 1 — Missing code argument or card not found
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { getLanguageCard, resolveCode } from '../registers.js';
|
|
23
|
+
|
|
24
|
+
// -----------------------------------------------------------------
|
|
25
|
+
// ANSI formatting helpers — built-in, no external deps
|
|
26
|
+
//
|
|
27
|
+
// We use raw ANSI escape sequences instead of chalk/kleur because
|
|
28
|
+
// the project rule is zero external dependencies. These helpers are
|
|
29
|
+
// intentionally simple — just enough for readable terminal output.
|
|
30
|
+
// -----------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const isTTY = process.stdout.isTTY;
|
|
33
|
+
|
|
34
|
+
/** Wrap text in an ANSI escape code pair, but only if stdout is a TTY. */
|
|
35
|
+
function ansi(code, resetCode, text) {
|
|
36
|
+
if (!isTTY) return text;
|
|
37
|
+
return `\x1b[${code}m${text}\x1b[${resetCode}m`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const fmt = {
|
|
41
|
+
bold: (t) => ansi('1', '22', t),
|
|
42
|
+
dim: (t) => ansi('2', '22', t),
|
|
43
|
+
cyan: (t) => ansi('36', '39', t),
|
|
44
|
+
green: (t) => ansi('32', '39', t),
|
|
45
|
+
yellow: (t) => ansi('33', '39', t),
|
|
46
|
+
red: (t) => ansi('31', '39', t),
|
|
47
|
+
magenta: (t) => ansi('35', '39', t),
|
|
48
|
+
white: (t) => ansi('37', '39', t),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// -----------------------------------------------------------------
|
|
52
|
+
// Section renderers — each handles one logical block of the card
|
|
53
|
+
// -----------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Print a section heading with a decorative underline.
|
|
57
|
+
* @param {string} title - Section title
|
|
58
|
+
*/
|
|
59
|
+
function heading(title) {
|
|
60
|
+
console.log('');
|
|
61
|
+
console.log(` ${fmt.bold(fmt.cyan(title))}`);
|
|
62
|
+
console.log(` ${fmt.dim('─'.repeat(title.length))}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Print a labeled key-value pair, with optional value formatting.
|
|
67
|
+
* @param {string} label - Field label (left-aligned)
|
|
68
|
+
* @param {*} value - Field value (auto-formatted)
|
|
69
|
+
* @param {number} [pad=24] - Label padding width
|
|
70
|
+
*/
|
|
71
|
+
function field(label, value, pad = 24) {
|
|
72
|
+
if (value === null || value === undefined || value === '') return;
|
|
73
|
+
let v = value;
|
|
74
|
+
// An attribution envelope reaching a human unrendered is a bug a unit test
|
|
75
|
+
// never sees and a user sees instantly — Family printed as raw JSON. Where
|
|
76
|
+
// sources AGREE the consensus speaks; where they DISAGREE the card shows all
|
|
77
|
+
// of them attributed, which is the project's standing rule, in one line.
|
|
78
|
+
if (v && typeof v === 'object' && typeof v.agreement === 'string' && Array.isArray(v.values)) {
|
|
79
|
+
v = 'consensus' in v
|
|
80
|
+
? v.consensus
|
|
81
|
+
: v.values.map((x) => `${x.value} (${String(x.source).replace(/-v?[\d.].*$/, '')})`).join(' / ');
|
|
82
|
+
}
|
|
83
|
+
const displayValue = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
84
|
+
console.log(` ${fmt.dim(label.padEnd(pad))} ${displayValue}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Print the header section: name, native name, codes, script, vitality.
|
|
89
|
+
*/
|
|
90
|
+
function renderHeader(card) {
|
|
91
|
+
// Title line — large and bold
|
|
92
|
+
const vitalityBadge = getVitalityBadge(card.vitality?.unescoStatus);
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(` ${fmt.bold(card.name)}${card.nativeName ? ` ${fmt.dim('—')} ${card.nativeName}` : ''}`);
|
|
95
|
+
if (vitalityBadge) {
|
|
96
|
+
console.log(` ${vitalityBadge}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
heading('Identification');
|
|
100
|
+
field('ISO 639-3', card.iso639_3);
|
|
101
|
+
field('ISO 639-1', card.iso639_1);
|
|
102
|
+
field('BCP 47', card.bcp47);
|
|
103
|
+
field('Glottocode', card.glottocode);
|
|
104
|
+
field('Script', card.script ? `${card.script}${card.scriptUnicodeName ? ` (${card.scriptUnicodeName})` : ''}` : null);
|
|
105
|
+
field('Direction', card.dir);
|
|
106
|
+
field('Script converter', card.scriptConverter);
|
|
107
|
+
field('Support tier', formatSupportTier(card.supportTier));
|
|
108
|
+
if (card.macrolanguage) {
|
|
109
|
+
field('Macrolanguage', card.macrolanguage);
|
|
110
|
+
}
|
|
111
|
+
if (card.aliases && card.aliases.length > 0) {
|
|
112
|
+
field('Aliases', card.aliases.join(', '));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Format UNESCO vitality status as a colored badge.
|
|
118
|
+
*/
|
|
119
|
+
function getVitalityBadge(status) {
|
|
120
|
+
if (!status) return null;
|
|
121
|
+
const badges = {
|
|
122
|
+
'safe': fmt.green('● safe'),
|
|
123
|
+
'vulnerable': fmt.yellow('● vulnerable'),
|
|
124
|
+
'definitely-endangered': fmt.yellow('● definitely endangered'),
|
|
125
|
+
'severely-endangered': fmt.red('● severely endangered'),
|
|
126
|
+
'critically-endangered': fmt.red('● critically endangered'),
|
|
127
|
+
'extinct': fmt.dim('○ extinct'),
|
|
128
|
+
};
|
|
129
|
+
return badges[status] || fmt.dim(`● ${status}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Format support tier with color.
|
|
134
|
+
*/
|
|
135
|
+
function formatSupportTier(tier) {
|
|
136
|
+
if (!tier) return null;
|
|
137
|
+
const tiers = {
|
|
138
|
+
'supported': fmt.green(tier),
|
|
139
|
+
'experimental': fmt.yellow(tier),
|
|
140
|
+
'developing': fmt.yellow(tier),
|
|
141
|
+
'cataloged': fmt.dim(tier),
|
|
142
|
+
'community': fmt.cyan(tier),
|
|
143
|
+
};
|
|
144
|
+
return tiers[tier] || tier;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Print classification section: family, genus, macroarea.
|
|
149
|
+
*/
|
|
150
|
+
function renderClassification(card) {
|
|
151
|
+
if (!card.classification && !card.macroarea) return;
|
|
152
|
+
|
|
153
|
+
heading('Classification');
|
|
154
|
+
if (card.classification) {
|
|
155
|
+
field('Family', card.classification.family);
|
|
156
|
+
field('Genus', card.classification.genus);
|
|
157
|
+
if (card.classification.ancestry) {
|
|
158
|
+
field('Ancestry', card.classification.ancestry.join(' → '));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
field('Macroarea', card.macroarea);
|
|
162
|
+
if (card.isIsolate) {
|
|
163
|
+
field('Language isolate', fmt.yellow('yes'));
|
|
164
|
+
}
|
|
165
|
+
if (card.countries && card.countries.length > 0) {
|
|
166
|
+
field('Countries', card.countries.join(', '));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Print speaker estimates section.
|
|
172
|
+
*/
|
|
173
|
+
function renderSpeakers(card) {
|
|
174
|
+
const estimates = card.speakerEstimates;
|
|
175
|
+
const vitalityCount = card.vitality?.speakerCount;
|
|
176
|
+
|
|
177
|
+
if ((!estimates || estimates.length === 0) && !vitalityCount) return;
|
|
178
|
+
|
|
179
|
+
heading('Speaker Estimates');
|
|
180
|
+
|
|
181
|
+
if (vitalityCount) {
|
|
182
|
+
field('Total (vitality)', typeof vitalityCount === 'number' ? vitalityCount.toLocaleString() : vitalityCount);
|
|
183
|
+
}
|
|
184
|
+
if (estimates) {
|
|
185
|
+
for (const est of estimates) {
|
|
186
|
+
const label = `${est.source}${est.date ? ` (${est.date})` : ''}`;
|
|
187
|
+
field(label, typeof est.count === 'number' ? est.count.toLocaleString() : est.count);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (card.vitality?.trend) {
|
|
191
|
+
field('Trend', card.vitality.trend);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Print typological profile section.
|
|
197
|
+
*/
|
|
198
|
+
function renderTypology(card) {
|
|
199
|
+
// Pull from encyclopedic.typology (richer) or typologicalProfile
|
|
200
|
+
const typ = card.encyclopedic?.typology;
|
|
201
|
+
const profile = card.typologicalProfile;
|
|
202
|
+
|
|
203
|
+
if (!typ && !profile) return;
|
|
204
|
+
|
|
205
|
+
heading('Typological Profile');
|
|
206
|
+
|
|
207
|
+
if (typ) {
|
|
208
|
+
field('Word order', typ.wordOrder);
|
|
209
|
+
field('Adpositions', typ.adpositionType);
|
|
210
|
+
field('Affixation', typ.affixType);
|
|
211
|
+
field('Verb synthesis', typ.verbSynthesis);
|
|
212
|
+
field('Morphological fusion', typ.morphologicalFusion);
|
|
213
|
+
field('Consonants', typ.consonantInventory);
|
|
214
|
+
field('Vowels', typ.vowelInventory);
|
|
215
|
+
field('Tone', typ.tone);
|
|
216
|
+
field('Cases', typ.cases);
|
|
217
|
+
field('Genders', typ.genders);
|
|
218
|
+
} else if (profile) {
|
|
219
|
+
// PRINT WHAT THE CARD CARRIES, not a list of field names typed in once.
|
|
220
|
+
//
|
|
221
|
+
// This used to name ten fixed keys — wordOrderDominant, hasGenderSystem,
|
|
222
|
+
// hasCaseMorphology and so on. The atlas emits the Grambank/WALS feature
|
|
223
|
+
// names instead (subjectVerbOrder, hasCoreCase, affixPreference,
|
|
224
|
+
// marksPastTense …), so every one of those lookups returned undefined and
|
|
225
|
+
// the section printed its heading over nothing while the card held twenty
|
|
226
|
+
// populated features.
|
|
227
|
+
//
|
|
228
|
+
// Enumerating the object cannot go stale that way: a feature the atlas
|
|
229
|
+
// starts publishing shows up without an edit here, and one it stops
|
|
230
|
+
// publishing disappears rather than printing blank.
|
|
231
|
+
const label = (key) => {
|
|
232
|
+
const spaced = key.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase();
|
|
233
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
234
|
+
};
|
|
235
|
+
for (const [key, value] of Object.entries(profile)) {
|
|
236
|
+
if (key === 'source' || key.startsWith('_')) continue;
|
|
237
|
+
if (value === null || value === undefined || value === '') continue;
|
|
238
|
+
field(label(key), typeof value === 'boolean' ? (value ? 'yes' : 'no') : value);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (profile?.source) {
|
|
243
|
+
field('Source', fmt.dim(profile.source));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Print corpus availability section.
|
|
249
|
+
*/
|
|
250
|
+
function renderCorpus(card) {
|
|
251
|
+
const corpus = card.corpusAvailability;
|
|
252
|
+
if (!corpus) return;
|
|
253
|
+
|
|
254
|
+
heading('Corpus Availability');
|
|
255
|
+
|
|
256
|
+
if (corpus.opus) {
|
|
257
|
+
const opusInfo = [];
|
|
258
|
+
if (corpus.opus.corpora) opusInfo.push(`${corpus.opus.corpora} corpora`);
|
|
259
|
+
if (corpus.opus.languagePairs) opusInfo.push(`${corpus.opus.languagePairs} lang pairs`);
|
|
260
|
+
if (corpus.opus.totalAlignmentPairs) opusInfo.push(`${corpus.opus.totalAlignmentPairs.toLocaleString()} alignment pairs`);
|
|
261
|
+
field('OPUS', opusInfo.join(', '));
|
|
262
|
+
if (corpus.opus.corpusNames && corpus.opus.corpusNames.length > 0) {
|
|
263
|
+
field(' Top corpora', corpus.opus.corpusNames.slice(0, 5).join(', '));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (corpus.lexibank) {
|
|
267
|
+
field('Lexibank', `${corpus.lexibank.datasets} datasets, ${corpus.lexibank.totalForms} forms`);
|
|
268
|
+
}
|
|
269
|
+
if (corpus.ud) {
|
|
270
|
+
field('Universal Deps', `${corpus.ud.treebanks} treebanks: ${corpus.ud.treebankNames?.join(', ') || ''}`);
|
|
271
|
+
}
|
|
272
|
+
if (corpus.asjpWordlists) field('ASJP', `${corpus.asjpWordlists} wordlist(s), ${corpus.asjpForms || '?'} forms`);
|
|
273
|
+
if (corpus.huggingFaceDatasets) field('HuggingFace', `${corpus.huggingFaceDatasets} datasets`);
|
|
274
|
+
if (corpus.unimorphParadigms) field('UniMorph', fmt.green('✓'));
|
|
275
|
+
if (corpus.wiktionaryStructuredDump) field('Wiktionary dump', fmt.green('✓'));
|
|
276
|
+
if (corpus.openMultilingualWordnet) field('Open Multilingual WN', fmt.green('✓'));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Print eval datasets section.
|
|
281
|
+
*/
|
|
282
|
+
function renderEval(card) {
|
|
283
|
+
if (!card.evalDatasets || card.evalDatasets.length === 0) return;
|
|
284
|
+
|
|
285
|
+
heading('Eval Datasets');
|
|
286
|
+
for (const ds of card.evalDatasets) {
|
|
287
|
+
console.log(` ${fmt.cyan('•')} ${ds}`);
|
|
288
|
+
}
|
|
289
|
+
if (card.omt1600) {
|
|
290
|
+
// The OMT-1600 paper publishes no per-language tier table, so `tier` is
|
|
291
|
+
// null on most covered languages. Print the clause only when there is a
|
|
292
|
+
// cited tier — never "tier null".
|
|
293
|
+
const tier = card.omt1600.tier ? ` — tier ${card.omt1600.tier}` : '';
|
|
294
|
+
field('OMT-1600', card.omt1600.covered ? `${fmt.green('covered')}${tier}` : fmt.dim('not covered'));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Print pipeline readiness section.
|
|
300
|
+
*/
|
|
301
|
+
function renderPipeline(card) {
|
|
302
|
+
const pr = card.pipelineReadiness;
|
|
303
|
+
if (!pr) return;
|
|
304
|
+
|
|
305
|
+
heading('Pipeline Readiness');
|
|
306
|
+
|
|
307
|
+
// Score bar — visual indicator of readiness
|
|
308
|
+
const score = typeof pr.score === 'number' ? pr.score : null;
|
|
309
|
+
if (score !== null) {
|
|
310
|
+
const barWidth = 20;
|
|
311
|
+
const filled = Math.round((score / 100) * barWidth);
|
|
312
|
+
const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
|
|
313
|
+
const colorFn = score >= 70 ? fmt.green : score >= 40 ? fmt.yellow : fmt.red;
|
|
314
|
+
field('Score', `${colorFn(bar)} ${score}/100 (${pr.tier || ''})`);
|
|
315
|
+
} else {
|
|
316
|
+
field('Tier', pr.tier);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Component checklist
|
|
320
|
+
if (pr.components) {
|
|
321
|
+
const checks = Object.entries(pr.components)
|
|
322
|
+
.map(([key, val]) => `${val ? fmt.green('✓') : fmt.dim('✗')} ${key}`)
|
|
323
|
+
.join(' ');
|
|
324
|
+
console.log(` ${checks}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Print method support section.
|
|
330
|
+
*/
|
|
331
|
+
function renderMethods(card) {
|
|
332
|
+
const methods = card.methodSupport;
|
|
333
|
+
if (!methods) return;
|
|
334
|
+
|
|
335
|
+
heading('Method Support');
|
|
336
|
+
|
|
337
|
+
const methodNames = {
|
|
338
|
+
googleTranslate: 'Google Translate',
|
|
339
|
+
deepl: 'DeepL',
|
|
340
|
+
microsoftTranslator: 'Microsoft Translator',
|
|
341
|
+
libreTranslate: 'LibreTranslate',
|
|
342
|
+
nllb: 'NLLB-200',
|
|
343
|
+
llm: 'LLM (OpenRouter)',
|
|
344
|
+
microsoft: 'Microsoft',
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
for (const [key, info] of Object.entries(methods)) {
|
|
348
|
+
const label = methodNames[key] || key;
|
|
349
|
+
const supported = info?.supported;
|
|
350
|
+
const badge = supported ? fmt.green('✓ supported') : fmt.dim('✗ unsupported');
|
|
351
|
+
let extra = '';
|
|
352
|
+
if (info?.formality) extra += ` ${fmt.cyan('(formality)')}`;
|
|
353
|
+
if (info?.code) extra += ` ${fmt.dim(`[${info.code}]`)}`;
|
|
354
|
+
if (info?.verifiedDate) extra += ` ${fmt.dim(`verified ${info.verifiedDate}`)}`;
|
|
355
|
+
field(label, `${badge}${extra}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Print data sources count and cultural aphorism.
|
|
361
|
+
*/
|
|
362
|
+
function renderFooter(card) {
|
|
363
|
+
// Data sources count
|
|
364
|
+
if (card.dataSources && card.dataSources.length > 0) {
|
|
365
|
+
heading('Data Sources');
|
|
366
|
+
field('Count', `${card.dataSources.length} sources`);
|
|
367
|
+
// Show first few and last few if there are many
|
|
368
|
+
if (card.dataSources.length <= 8) {
|
|
369
|
+
field('Sources', card.dataSources.join(', '));
|
|
370
|
+
} else {
|
|
371
|
+
const preview = [...card.dataSources.slice(0, 5), `… +${card.dataSources.length - 5} more`];
|
|
372
|
+
field('Sources', preview.join(', '));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Cultural aphorism — a nice touch at the bottom
|
|
377
|
+
if (card.culturalAphorism) {
|
|
378
|
+
heading('Cultural Aphorism');
|
|
379
|
+
console.log(` ${fmt.magenta(`"${card.culturalAphorism.text}"`)}`);
|
|
380
|
+
if (card.culturalAphorism.translation) {
|
|
381
|
+
console.log(` ${fmt.dim(`— "${card.culturalAphorism.translation}"`)}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Notes
|
|
386
|
+
if (card.notes) {
|
|
387
|
+
console.log('');
|
|
388
|
+
console.log(` ${fmt.dim('Note:')} ${fmt.dim(card.notes)}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
console.log('');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// -----------------------------------------------------------------
|
|
395
|
+
// Main entry point
|
|
396
|
+
// -----------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* @param {import('../types.js').CLIArgs} args - Parsed CLI arguments
|
|
400
|
+
* @param {string} cwd - Working directory
|
|
401
|
+
* @returns {Promise<number>} Exit code (0 = success, 1 = error)
|
|
402
|
+
*/
|
|
403
|
+
async function run(args, cwd) {
|
|
404
|
+
const code = args._[1];
|
|
405
|
+
|
|
406
|
+
// ── Validate: code argument is required ──
|
|
407
|
+
if (!code) {
|
|
408
|
+
console.error('[ERR] Missing language code argument.');
|
|
409
|
+
console.error('');
|
|
410
|
+
console.error(' Usage: champollion card <code> [--json]');
|
|
411
|
+
console.error('');
|
|
412
|
+
console.error(' Examples:');
|
|
413
|
+
console.error(' champollion card crk # Plains Cree');
|
|
414
|
+
console.error(' champollion card spa # Spanish');
|
|
415
|
+
console.error(' champollion card cmn --json # Raw JSON');
|
|
416
|
+
console.error('');
|
|
417
|
+
return 1;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── Resolve the code (follow aliases like 'fr' → 'fra') ──
|
|
421
|
+
const resolvedCode = resolveCode(code);
|
|
422
|
+
const card = getLanguageCard(resolvedCode);
|
|
423
|
+
|
|
424
|
+
if (!card) {
|
|
425
|
+
console.error(`[ERR] No language card found for "${code}".`);
|
|
426
|
+
if (code !== resolvedCode) {
|
|
427
|
+
console.error(` (resolved alias to "${resolvedCode}", but no card exists)`);
|
|
428
|
+
}
|
|
429
|
+
console.error('');
|
|
430
|
+
console.error(' Run "champollion doctor cards" to check card coverage.');
|
|
431
|
+
return 1;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── --json mode: output raw JSON and exit ──
|
|
435
|
+
if (args.json) {
|
|
436
|
+
console.log(JSON.stringify(card, null, 2));
|
|
437
|
+
return 0;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── Pretty-print mode: render each section ──
|
|
441
|
+
renderHeader(card);
|
|
442
|
+
renderClassification(card);
|
|
443
|
+
renderSpeakers(card);
|
|
444
|
+
renderTypology(card);
|
|
445
|
+
renderCorpus(card);
|
|
446
|
+
renderEval(card);
|
|
447
|
+
renderPipeline(card);
|
|
448
|
+
renderMethods(card);
|
|
449
|
+
renderFooter(card);
|
|
450
|
+
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export { run };
|