champollion 0.3.4 → 0.4.0
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/README.md +41 -26
- package/bin/cli.js +53 -5
- package/index.js +63 -2
- package/lib/api-key.js +17 -4
- package/lib/autofix.js +83 -36
- package/lib/bridge/method_bridge.py +15 -3
- package/lib/cards/reader.js +34 -0
- package/lib/cards/remote.js +15 -0
- package/lib/cards/search-names.js +178 -0
- package/lib/command-help.js +286 -85
- package/lib/commands/audit.js +10 -3
- package/lib/commands/card.js +583 -226
- package/lib/commands/doctor.js +54 -18
- package/lib/commands/help.js +37 -32
- package/lib/commands/init.js +1689 -87
- package/lib/commands/integrity.js +127 -40
- package/lib/commands/leaderboard.js +187 -67
- package/lib/commands/models.js +9 -2
- package/lib/commands/provenance.js +7 -2
- package/lib/commands/recommend.js +43 -14
- package/lib/commands/register-corpus.js +632 -125
- package/lib/commands/seal-corpus.js +1 -1
- package/lib/commands/status.js +564 -27
- package/lib/commands/submit.js +17 -12
- package/lib/commands/sync.js +31 -7
- package/lib/commands/tm.js +15 -9
- package/lib/commands/verify.js +27 -3
- package/lib/commands/wrap.js +63 -5
- package/lib/commands/xliff.js +135 -64
- package/lib/commercial-eligibility.js +1 -1
- package/lib/config.js +196 -14
- package/lib/content-estimate.js +96 -0
- package/lib/content-refusals.js +270 -0
- package/lib/content-review.js +372 -0
- package/lib/content-sync.js +1127 -344
- package/lib/content.js +94 -7
- package/lib/corpus-registration.mjs +194 -35
- package/lib/cost-label.js +29 -0
- package/lib/cost-report.js +726 -78
- package/lib/diff.js +38 -4
- package/lib/docusaurus-sync.js +965 -253
- package/lib/edit-distance.js +31 -0
- package/lib/fallback.js +964 -0
- package/lib/file-scope.js +106 -0
- package/lib/flatten.js +80 -3
- package/lib/flutter-locales.js +124 -0
- package/lib/format.js +266 -12
- package/lib/hash.js +146 -21
- package/lib/icu-structure.js +929 -0
- package/lib/integrity.js +223 -75
- package/lib/language-pair.js +157 -0
- package/lib/lint.js +78 -16
- package/lib/local-only-marks.js +106 -0
- package/lib/locale-layout.js +1103 -0
- package/lib/locale-state.js +571 -0
- package/lib/methods/anthropic.js +5 -0
- package/lib/methods/apertium.js +6 -3
- package/lib/methods/api.js +138 -25
- package/lib/methods/base.js +17 -0
- package/lib/methods/coaching-data.js +153 -0
- package/lib/methods/content-separator.js +43 -0
- package/lib/methods/deepl.js +1 -1
- package/lib/methods/direct-llm.js +252 -103
- package/lib/methods/external.js +146 -63
- package/lib/methods/gemini.js +1 -0
- package/lib/methods/google-translate.js +1 -0
- package/lib/methods/http-utils.js +41 -0
- package/lib/methods/libretranslate.js +7 -2
- package/lib/methods/llm-coached.js +68 -128
- package/lib/methods/llm.js +80 -31
- package/lib/methods/local.js +93 -10
- package/lib/methods/microsoft-translator.js +1 -2
- package/lib/methods/openai.js +4 -2
- package/lib/methods/openrouter-client.js +20 -19
- package/lib/methods/openrouter-pricing.js +150 -13
- package/lib/methods/prompt-methods.js +20 -0
- package/lib/methods/provider-pricing.js +42 -1
- package/lib/methods/request-capture.js +104 -0
- package/lib/methods/tilde.js +1 -1
- package/lib/methods/translated.js +1 -2
- package/lib/missing-key.js +93 -0
- package/lib/models.js +11 -0
- package/lib/name-rules.js +32 -0
- package/lib/named-keys.js +172 -0
- package/lib/no-translate.js +4 -3
- package/lib/output.js +160 -19
- package/lib/pairs.js +586 -30
- package/lib/placeholders.js +394 -0
- package/lib/plugins.js +8 -0
- package/lib/plural-gap-redo.js +109 -0
- package/lib/plurals.js +323 -0
- package/lib/po.js +1187 -0
- package/lib/public-catalogue.js +74 -0
- package/lib/recommend.js +527 -32
- package/lib/redo.js +95 -0
- package/lib/refusal-category.js +44 -0
- package/lib/registers.js +255 -11
- package/lib/repair-script.js +20 -13
- package/lib/scripts.js +6 -1
- package/lib/seal.mjs +4 -3
- package/lib/sealed-qualifier.mjs +1 -1
- package/lib/segment.js +2 -1
- package/lib/seo.js +19 -9
- package/lib/serve.js +43 -6
- package/lib/shared-output-seed.js +164 -0
- package/lib/source-contexts.js +39 -0
- package/lib/submit.mjs +57 -5
- package/lib/sync.js +2923 -474
- package/lib/terminology.js +13 -4
- package/lib/tm-evict.js +179 -0
- package/lib/tm-seed.js +5 -2
- package/lib/tm.js +818 -36
- package/lib/translate-pair.js +639 -34
- package/lib/translate.js +78 -5
- package/lib/types.js +22 -3
- package/lib/validate.js +880 -17
- package/lib/verify.js +1296 -104
- package/lib/watch.js +32 -13
- package/lib/xliff.js +44 -3
- package/package.json +1 -1
- package/shared/CORPORA-CARDS.md +2 -0
- package/shared/cards-fallback.json +1 -1
- package/shared/curated-orthography-conventions.json +26 -8
- package/shared/gettext-plural-forms.json +45 -0
- package/shared/method-registry.json +2 -0
- package/shared/metric-registry.json +96 -18
- package/shared/schemas/champollion-plugin.schema.json +4 -0
- package/shared/schemas/corpora-card.schema.json +8 -2
- package/shared/schemas/method-index-record.schema.json +67 -0
- package/shared/schemas/method-registry.schema.json +4 -0
- package/shared/schemas/metric-registry.schema.json +55 -1
- package/shared/docent/corpus.json +0 -11739
package/lib/commands/card.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Pretty-prints a language card from shared/language-cards/.
|
|
5
5
|
*
|
|
6
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
|
|
7
|
+
* champollion network card crk — Show Plains Cree card
|
|
8
|
+
* champollion network card spa — Show Spanish card
|
|
9
|
+
* champollion network card crk --json — Output raw JSON
|
|
10
10
|
*
|
|
11
11
|
* WHY: Language cards are deeply nested JSON files with 50+ fields.
|
|
12
12
|
* Developers and contributors need a quick way to inspect a card's
|
|
@@ -14,12 +14,31 @@
|
|
|
14
14
|
* formats the card into logical sections with ANSI colors for
|
|
15
15
|
* scannability.
|
|
16
16
|
*
|
|
17
|
+
* TWO RULES THE OUTPUT KEEPS (the index invariant, made visible):
|
|
18
|
+
* 1. Where sources disagree, every value is printed with its source and
|
|
19
|
+
* none is elected. A disputed attribution envelope never collapses to
|
|
20
|
+
* one line — endangerment printed as a single "● vulnerable" once hid
|
|
21
|
+
* five assessments on three scales for Plains Cree.
|
|
22
|
+
* 2. A section heading is never printed over nothing. Absence on a card
|
|
23
|
+
* means UNKNOWN, so an empty section says "not recorded on this card"
|
|
24
|
+
* and, where there is one, names the command that answers the question
|
|
25
|
+
* for a pair. A bare heading reads as a rendering failure.
|
|
26
|
+
*
|
|
17
27
|
* Exit codes:
|
|
18
28
|
* 0 — Card displayed successfully
|
|
19
29
|
* 1 — Missing code argument or card not found
|
|
20
30
|
*/
|
|
21
31
|
|
|
22
32
|
import { getLanguageCard, resolveCode } from '../registers.js';
|
|
33
|
+
import {
|
|
34
|
+
AGREEMENT,
|
|
35
|
+
attributedFields,
|
|
36
|
+
attributions,
|
|
37
|
+
display,
|
|
38
|
+
isAttributed,
|
|
39
|
+
isDisputed,
|
|
40
|
+
readCard,
|
|
41
|
+
} from '../cards/reader.js';
|
|
23
42
|
|
|
24
43
|
// -----------------------------------------------------------------
|
|
25
44
|
// ANSI formatting helpers — built-in, no external deps
|
|
@@ -48,69 +67,229 @@ const fmt = {
|
|
|
48
67
|
white: (t) => ansi('37', '39', t),
|
|
49
68
|
};
|
|
50
69
|
|
|
70
|
+
const PAD = 24;
|
|
71
|
+
const NOTE_MAX = 100;
|
|
72
|
+
const LIST_CAP = 5;
|
|
73
|
+
|
|
74
|
+
/** The words for an empty section. Absence is unknown — never "none". */
|
|
75
|
+
const NOT_RECORDED = 'not recorded on this card (unknown, not "none")';
|
|
76
|
+
|
|
77
|
+
/** What a disagreement looks like, in the reader's words, per agreement. */
|
|
78
|
+
const AGREEMENT_NOTE = {
|
|
79
|
+
[AGREEMENT.CONFLICTING]: 'sources differ — every value shown, none elected',
|
|
80
|
+
[AGREEMENT.INCOMMENSURABLE]: 'sources use different scales — every value shown, none elected',
|
|
81
|
+
[AGREEMENT.MULTIPLE_ASSESSMENTS]: 'several assessments — every value shown',
|
|
82
|
+
[AGREEMENT.MULTIPLE_VARIANTS]: 'several variants — every value shown',
|
|
83
|
+
[AGREEMENT.UNANIMOUS]: 'sources agree',
|
|
84
|
+
};
|
|
85
|
+
|
|
51
86
|
// -----------------------------------------------------------------
|
|
52
|
-
//
|
|
87
|
+
// Output primitives — every renderer appends to ctx.out
|
|
53
88
|
// -----------------------------------------------------------------
|
|
54
89
|
|
|
55
90
|
/**
|
|
56
91
|
* Print a section heading with a decorative underline.
|
|
57
|
-
* @param {string} title - Section title
|
|
58
92
|
*/
|
|
59
|
-
function heading(title) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
93
|
+
function heading(ctx, title) {
|
|
94
|
+
ctx.out.push('');
|
|
95
|
+
ctx.out.push(` ${fmt.bold(fmt.cyan(title))}`);
|
|
96
|
+
ctx.out.push(` ${fmt.dim('─'.repeat(title.length))}`);
|
|
63
97
|
}
|
|
64
98
|
|
|
65
99
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
100
|
+
* A section whose body is guaranteed to say something.
|
|
101
|
+
*
|
|
102
|
+
* If the body prints nothing, the empty lines are printed instead — so a
|
|
103
|
+
* heading can never stand over a blank, whatever shape the card arrived in
|
|
104
|
+
* (the published projection ships `{}` for fields no source fills).
|
|
105
|
+
*
|
|
106
|
+
* @param {object} ctx
|
|
107
|
+
* @param {string} title
|
|
108
|
+
* @param {() => void} body
|
|
109
|
+
* @param {string[]} [emptyLines]
|
|
70
110
|
*/
|
|
71
|
-
function
|
|
111
|
+
function section(ctx, title, body, emptyLines = [NOT_RECORDED]) {
|
|
112
|
+
heading(ctx, title);
|
|
113
|
+
const before = ctx.out.length;
|
|
114
|
+
body();
|
|
115
|
+
if (ctx.out.length === before) {
|
|
116
|
+
for (const line of emptyLines) ctx.out.push(` ${fmt.dim(line)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** True when an envelope has no single value a reader may be shown alone. */
|
|
121
|
+
function needsAllValues(env) {
|
|
122
|
+
return isDisputed(env) || display(env) === undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A claim's note as one short line, or '' when there is none.
|
|
127
|
+
*
|
|
128
|
+
* Notes carry scope ("British Columbia only") and certainty, which is why
|
|
129
|
+
* they are shown at all; some are WALS example HTML, so tags go.
|
|
130
|
+
*/
|
|
131
|
+
function noteText(note) {
|
|
132
|
+
if (!note) return '';
|
|
133
|
+
const flat = String(note).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
134
|
+
return flat.length > NOTE_MAX ? `${flat.slice(0, NOTE_MAX - 1)}…` : flat;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** One value-with-source, as a printable line fragment. */
|
|
138
|
+
function claimText(c) {
|
|
139
|
+
const v = c?.value ?? c?.count;
|
|
140
|
+
const value = v === null || v === undefined || v === ''
|
|
141
|
+
? '(empty)'
|
|
142
|
+
: typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
143
|
+
const variant = c?.variant ? ` (${c.variant})` : '';
|
|
144
|
+
const when = c?.year ?? c?.date;
|
|
145
|
+
const date = when ? ` (${when})` : '';
|
|
146
|
+
const src = c?.source ? `[${c.source}]` : '[source not stated]';
|
|
147
|
+
const note = noteText(c?.note);
|
|
148
|
+
return `${value}${variant}${date} ${fmt.dim(`${src}${note ? ` — ${note}` : ''}`)}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Every claim on its own line under a label, with what the agreement means. */
|
|
152
|
+
function claimsList(ctx, label, claims, agreementNote) {
|
|
153
|
+
ctx.out.push(` ${fmt.dim(label.padEnd(PAD))} ${fmt.yellow(agreementNote)}`);
|
|
154
|
+
for (const c of claims) ctx.out.push(` ${fmt.cyan('•')} ${claimText(c)}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Print a labeled value.
|
|
159
|
+
*
|
|
160
|
+
* An attribution envelope where sources agree prints its consensus on one
|
|
161
|
+
* line. Where they disagree — or where there is no consensus at all — every
|
|
162
|
+
* value is printed with its source, one per line. Electing one, or squashing
|
|
163
|
+
* them into a single line a reader skims as an answer, is the failure this
|
|
164
|
+
* exists to prevent.
|
|
165
|
+
*/
|
|
166
|
+
function field(ctx, label, value) {
|
|
72
167
|
if (value === null || value === undefined || value === '') return;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
: v.values.map((x) => `${x.value} (${String(x.source).replace(/-v?[\d.].*$/, '')})`).join(' / ');
|
|
168
|
+
if (isAttributed(value)) {
|
|
169
|
+
// display() throws on an agreement it does not know — loud by design.
|
|
170
|
+
if (needsAllValues(value)) {
|
|
171
|
+
claimsList(ctx, label, attributions(value),
|
|
172
|
+
AGREEMENT_NOTE[value.agreement] ?? 'every value shown');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
value = display(value);
|
|
82
176
|
}
|
|
83
|
-
const displayValue = typeof
|
|
84
|
-
|
|
177
|
+
const displayValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
178
|
+
ctx.out.push(` ${fmt.dim(label.padEnd(PAD))} ${displayValue}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** A label followed by several lines, the first beside it, the rest aligned. */
|
|
182
|
+
function listField(ctx, label, lines, cap = LIST_CAP) {
|
|
183
|
+
if (!lines.length) return;
|
|
184
|
+
const shown = lines.slice(0, cap);
|
|
185
|
+
if (lines.length > cap) shown.push(fmt.dim(`… +${lines.length - cap} more (--json for all)`));
|
|
186
|
+
shown.forEach((line, i) => {
|
|
187
|
+
ctx.out.push(` ${fmt.dim((i === 0 ? label : '').padEnd(PAD))} ${line}`);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Objects only, from something that may not be an array. */
|
|
192
|
+
function listOf(v) {
|
|
193
|
+
return Array.isArray(v) ? v.filter((x) => x && typeof x === 'object') : [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** camelCase key → "Sentence case" label. */
|
|
197
|
+
function labelFor(key) {
|
|
198
|
+
const spaced = key.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase();
|
|
199
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The code a pair command takes for this card (a locale's is its language). */
|
|
203
|
+
function pairCode(card) {
|
|
204
|
+
return card.iso639_3 ?? card.locale?.language ?? card.code;
|
|
85
205
|
}
|
|
86
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Every endangerment assessment the card carries.
|
|
209
|
+
*
|
|
210
|
+
* The atlas card holds an envelope; the published projection writes
|
|
211
|
+
* `{agreement: null, values: []}` when no source assesses the language,
|
|
212
|
+
* which is not an envelope and must read as "none", not as one odd value.
|
|
213
|
+
*/
|
|
214
|
+
function endangermentClaims(card) {
|
|
215
|
+
const env = card.endangerment;
|
|
216
|
+
if (isAttributed(env)) return attributions(env);
|
|
217
|
+
if (env && typeof env === 'object' && Array.isArray(env.values)) return listOf(env.values);
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// -----------------------------------------------------------------
|
|
222
|
+
// Section renderers — each handles one logical block of the card
|
|
223
|
+
// -----------------------------------------------------------------
|
|
224
|
+
|
|
87
225
|
/**
|
|
88
226
|
* Print the header section: name, native name, codes, script, vitality.
|
|
89
227
|
*/
|
|
90
|
-
function renderHeader(card) {
|
|
91
|
-
|
|
228
|
+
function renderHeader(card, ctx) {
|
|
229
|
+
const raw = ctx.rawCard;
|
|
230
|
+
const title = typeof card.name === 'string'
|
|
231
|
+
? card.name
|
|
232
|
+
: display(card.name, { onDisagreement: 'first' }) ?? card.code;
|
|
233
|
+
const native = typeof card.nativeName === 'string' ? card.nativeName : null;
|
|
234
|
+
ctx.out.push('');
|
|
235
|
+
ctx.out.push(` ${fmt.bold(title)}${native ? ` ${fmt.dim('—')} ${native}` : ''}`);
|
|
236
|
+
|
|
237
|
+
// The badge is ONE display tier the reader derives from one cited source in
|
|
238
|
+
// a declared authority order. Printed bare, it reads as the index's verdict;
|
|
239
|
+
// so it says where it came from, and that the other assessments exist.
|
|
92
240
|
const vitalityBadge = getVitalityBadge(card.vitality?.unescoStatus);
|
|
93
|
-
console.log('');
|
|
94
|
-
console.log(` ${fmt.bold(card.name)}${card.nativeName ? ` ${fmt.dim('—')} ${card.nativeName}` : ''}`);
|
|
95
241
|
if (vitalityBadge) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
field('Script converter', card.scriptConverter);
|
|
107
|
-
field('Support tier', formatSupportTier(card.supportTier));
|
|
108
|
-
if (card.macrolanguage) {
|
|
109
|
-
field('Macrolanguage', card.macrolanguage);
|
|
242
|
+
const by = card.vitality?.assessedBy;
|
|
243
|
+
const claims = endangermentClaims(card);
|
|
244
|
+
const differ = new Set(claims.map((c) => String(c.value).toLowerCase())).size > 1;
|
|
245
|
+
let qualifier = by ? `display tier, champollion-derived from ${by}` : 'display tier, champollion-derived';
|
|
246
|
+
if (claims.length > 1) {
|
|
247
|
+
qualifier += differ
|
|
248
|
+
? ` — ${claims.length} assessments differ, all shown under Endangerment`
|
|
249
|
+
: ` — ${claims.length} assessments under Endangerment`;
|
|
250
|
+
}
|
|
251
|
+
ctx.out.push(` ${vitalityBadge} ${fmt.dim(`(${qualifier})`)}`);
|
|
110
252
|
}
|
|
111
|
-
if (card.
|
|
112
|
-
|
|
253
|
+
if (card._remote) {
|
|
254
|
+
// A card rebuilt from the published tables carries fewer fields than the
|
|
255
|
+
// atlas card it was projected from, so its blanks are blanker still.
|
|
256
|
+
const when = card._remote.updatedAt ? `, published ${String(card._remote.updatedAt).slice(0, 10)}` : '';
|
|
257
|
+
ctx.out.push(` ${fmt.dim(`Served from champollion.dev's published card projection${when}: it carries fewer fields than the full atlas card, so anything absent below is unknown, not "none".`)}`);
|
|
113
258
|
}
|
|
259
|
+
|
|
260
|
+
section(ctx, 'Identification', () => {
|
|
261
|
+
// The title above is a LABEL: where registries disagree on the name, the
|
|
262
|
+
// reader takes the first value so lists and logs have something to say.
|
|
263
|
+
// The disagreement itself is printed here. Only the on-disk atlas card
|
|
264
|
+
// still holds the name envelope (the registry view flattened it).
|
|
265
|
+
const nameEnv = isAttributed(raw?.name) ? raw.name : isAttributed(card.name) ? card.name : null;
|
|
266
|
+
if (nameEnv && needsAllValues(nameEnv)) {
|
|
267
|
+
field(ctx, 'Name', nameEnv);
|
|
268
|
+
ctx.rendered.add('name');
|
|
269
|
+
}
|
|
270
|
+
const endonymEnv = isAttributed(raw?.endonym) ? raw.endonym
|
|
271
|
+
: isAttributed(card.endonym) ? card.endonym : null;
|
|
272
|
+
if (endonymEnv && needsAllValues(endonymEnv)) {
|
|
273
|
+
field(ctx, 'Endonym', endonymEnv);
|
|
274
|
+
ctx.rendered.add('endonym');
|
|
275
|
+
}
|
|
276
|
+
field(ctx, 'ISO 639-3', card.iso639_3);
|
|
277
|
+
field(ctx, 'ISO 639-1', card.iso639_1);
|
|
278
|
+
field(ctx, 'BCP 47', card.bcp47);
|
|
279
|
+
field(ctx, 'Glottocode', card.glottocode);
|
|
280
|
+
field(ctx, 'Script', card.script ? `${card.script}${card.scriptUnicodeName ? ` (${card.scriptUnicodeName})` : ''}` : null);
|
|
281
|
+
field(ctx, 'Direction', card.dir);
|
|
282
|
+
field(ctx, 'Script converter', card.scriptConverter);
|
|
283
|
+
field(ctx, 'Support tier', formatSupportTier(card.supportTier));
|
|
284
|
+
if (card.macrolanguage) {
|
|
285
|
+
field(ctx, 'Macrolanguage', card.macrolanguage);
|
|
286
|
+
}
|
|
287
|
+
if (card.aliases && card.aliases.length > 0) {
|
|
288
|
+
field(ctx, 'Aliases', card.aliases.join(', '));
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
ctx.rendered.add('name');
|
|
292
|
+
ctx.rendered.add('endonym');
|
|
114
293
|
}
|
|
115
294
|
|
|
116
295
|
/**
|
|
@@ -147,193 +326,297 @@ function formatSupportTier(tier) {
|
|
|
147
326
|
/**
|
|
148
327
|
* Print classification section: family, genus, macroarea.
|
|
149
328
|
*/
|
|
150
|
-
function renderClassification(card) {
|
|
329
|
+
function renderClassification(card, ctx) {
|
|
151
330
|
if (!card.classification && !card.macroarea) return;
|
|
152
331
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
332
|
+
section(ctx, 'Classification', () => {
|
|
333
|
+
const cls = card.classification;
|
|
334
|
+
if (cls) {
|
|
335
|
+
// The published projection carries family as a label plus the full
|
|
336
|
+
// attribution list; the atlas card carries the envelope itself.
|
|
337
|
+
const famClaims = listOf(cls.familyAttributions);
|
|
338
|
+
if (!isAttributed(cls.family)
|
|
339
|
+
&& new Set(famClaims.map((c) => String(c.value))).size > 1) {
|
|
340
|
+
claimsList(ctx, 'Family', famClaims, AGREEMENT_NOTE[AGREEMENT.CONFLICTING]);
|
|
341
|
+
} else {
|
|
342
|
+
field(ctx, 'Family', cls.family);
|
|
343
|
+
}
|
|
344
|
+
field(ctx, 'Genus', cls.genus);
|
|
345
|
+
if (Array.isArray(cls.ancestry) && cls.ancestry.length) {
|
|
346
|
+
field(ctx, 'Ancestry', cls.ancestry.join(' → '));
|
|
347
|
+
}
|
|
159
348
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
349
|
+
field(ctx, 'Macroarea', card.macroarea);
|
|
350
|
+
if (card.isIsolate) {
|
|
351
|
+
field(ctx, 'Language isolate', fmt.yellow('yes'));
|
|
352
|
+
}
|
|
353
|
+
if (card.countries && card.countries.length > 0) {
|
|
354
|
+
field(ctx, 'Countries', card.countries.join(', '));
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
ctx.rendered.add('classification.family');
|
|
358
|
+
ctx.rendered.add('classification.genus');
|
|
168
359
|
}
|
|
169
360
|
|
|
170
361
|
/**
|
|
171
362
|
* Print speaker estimates section.
|
|
172
363
|
*/
|
|
173
|
-
function renderSpeakers(card) {
|
|
174
|
-
|
|
364
|
+
function renderSpeakers(card, ctx) {
|
|
365
|
+
// normalizeCard turns the envelope into a list of {count, source, date,
|
|
366
|
+
// note}; a card that skipped the adapter still carries the envelope.
|
|
367
|
+
const estimates = isAttributed(card.speakerEstimates)
|
|
368
|
+
? attributions(card.speakerEstimates).map((c) => ({
|
|
369
|
+
count: c.value, source: c.source, date: c.year, note: c.note,
|
|
370
|
+
}))
|
|
371
|
+
: listOf(card.speakerEstimates);
|
|
175
372
|
const vitalityCount = card.vitality?.speakerCount;
|
|
176
373
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (estimates) {
|
|
374
|
+
section(ctx, 'Speaker Estimates', () => {
|
|
375
|
+
if (vitalityCount) {
|
|
376
|
+
field(ctx, 'Total (vitality)', typeof vitalityCount === 'number' ? vitalityCount.toLocaleString() : vitalityCount);
|
|
377
|
+
}
|
|
378
|
+
if (estimates.length > 1 && new Set(estimates.map((e) => String(e.count))).size > 1) {
|
|
379
|
+
ctx.out.push(` ${fmt.yellow('sources differ — every claim shown, none elected')}`);
|
|
380
|
+
}
|
|
185
381
|
for (const est of estimates) {
|
|
186
|
-
const label = `${est.source}${est.date ? ` (${est.date})` : ''}`;
|
|
187
|
-
|
|
382
|
+
const label = `${est.source ?? 'source not stated'}${est.date ? ` (${est.date})` : ''}`;
|
|
383
|
+
const count = typeof est.count === 'number' ? est.count.toLocaleString() : est.count;
|
|
384
|
+
// The note is the claim's scope ("British Columbia only") — dropping it
|
|
385
|
+
// once made a provincial count read as the language's total.
|
|
386
|
+
const note = noteText(est.note);
|
|
387
|
+
field(ctx, label, `${count}${note ? fmt.dim(` — ${note}`) : ''}`);
|
|
188
388
|
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
389
|
+
}, ['no cited estimate on this card (unknown, not zero)']);
|
|
390
|
+
ctx.rendered.add('speakerEstimates');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Print every endangerment assessment the card carries, each on its own
|
|
395
|
+
* source's scale, then the one display tier the reader derives from them.
|
|
396
|
+
*/
|
|
397
|
+
function renderEndangerment(card, ctx) {
|
|
398
|
+
section(ctx, 'Endangerment', () => {
|
|
399
|
+
const claims = endangermentClaims(card);
|
|
400
|
+
if (claims.length === 1) {
|
|
401
|
+
field(ctx, 'Assessment', claimText(claims[0]));
|
|
402
|
+
} else if (claims.length > 1) {
|
|
403
|
+
const agreement = isAttributed(card.endangerment) ? card.endangerment.agreement : null;
|
|
404
|
+
const differ = new Set(claims.map((c) => String(c.value).toLowerCase())).size > 1;
|
|
405
|
+
claimsList(ctx, 'Assessments', claims,
|
|
406
|
+
AGREEMENT_NOTE[agreement] ?? (differ ? AGREEMENT_NOTE[AGREEMENT.CONFLICTING] : 'every value shown'));
|
|
407
|
+
}
|
|
408
|
+
if (card.vitality?.unescoStatus) {
|
|
409
|
+
const by = card.vitality.assessedBy;
|
|
410
|
+
field(ctx, 'Display tier',
|
|
411
|
+
`${card.vitality.unescoStatus} ${fmt.dim(`— champollion-derived from ${by ?? 'one cited assessment'}; a reading of one source, not a consensus`)}`);
|
|
412
|
+
if (!claims.length) {
|
|
413
|
+
ctx.out.push(` ${fmt.dim('the per-source assessments are not carried by this card')}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}, ['no source on this card assesses it (unknown, never "safe")']);
|
|
417
|
+
ctx.rendered.add('endangerment');
|
|
193
418
|
}
|
|
194
419
|
|
|
195
420
|
/**
|
|
196
421
|
* Print typological profile section.
|
|
197
422
|
*/
|
|
198
|
-
function renderTypology(card) {
|
|
423
|
+
function renderTypology(card, ctx) {
|
|
199
424
|
// Pull from encyclopedic.typology (richer) or typologicalProfile
|
|
200
425
|
const typ = card.encyclopedic?.typology;
|
|
201
426
|
const profile = card.typologicalProfile;
|
|
202
427
|
|
|
203
428
|
if (!typ && !profile) return;
|
|
204
429
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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);
|
|
430
|
+
section(ctx, 'Typological Profile', () => {
|
|
431
|
+
if (typ) {
|
|
432
|
+
field(ctx, 'Word order', typ.wordOrder);
|
|
433
|
+
field(ctx, 'Adpositions', typ.adpositionType);
|
|
434
|
+
field(ctx, 'Affixation', typ.affixType);
|
|
435
|
+
field(ctx, 'Verb synthesis', typ.verbSynthesis);
|
|
436
|
+
field(ctx, 'Morphological fusion', typ.morphologicalFusion);
|
|
437
|
+
field(ctx, 'Consonants', typ.consonantInventory);
|
|
438
|
+
field(ctx, 'Vowels', typ.vowelInventory);
|
|
439
|
+
field(ctx, 'Tone', typ.tone);
|
|
440
|
+
field(ctx, 'Cases', typ.cases);
|
|
441
|
+
field(ctx, 'Genders', typ.genders);
|
|
442
|
+
} else if (profile) {
|
|
443
|
+
// PRINT WHAT THE CARD CARRIES, not a list of field names typed in once.
|
|
444
|
+
//
|
|
445
|
+
// This used to name ten fixed keys — wordOrderDominant, hasGenderSystem,
|
|
446
|
+
// hasCaseMorphology and so on. The atlas emits the Grambank/WALS feature
|
|
447
|
+
// names instead (subjectVerbOrder, hasCoreCase, affixPreference,
|
|
448
|
+
// marksPastTense …), so every one of those lookups returned undefined and
|
|
449
|
+
// the section printed its heading over nothing while the card held twenty
|
|
450
|
+
// populated features.
|
|
451
|
+
//
|
|
452
|
+
// Enumerating the object cannot go stale that way: a feature the atlas
|
|
453
|
+
// starts publishing shows up without an edit here, and one it stops
|
|
454
|
+
// publishing disappears rather than printing blank.
|
|
455
|
+
for (const [key, value] of Object.entries(profile)) {
|
|
456
|
+
if (key === 'source' || key.startsWith('_')) continue;
|
|
457
|
+
if (value === null || value === undefined || value === '') continue;
|
|
458
|
+
field(ctx, labelFor(key), typeof value === 'boolean' ? (value ? 'yes' : 'no') : value);
|
|
459
|
+
ctx.rendered.add(`typologicalProfile.${key}`);
|
|
460
|
+
}
|
|
239
461
|
}
|
|
240
|
-
}
|
|
241
462
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
463
|
+
if (profile?.source) {
|
|
464
|
+
field(ctx, 'Source', fmt.dim(profile.source));
|
|
465
|
+
}
|
|
466
|
+
});
|
|
245
467
|
}
|
|
246
468
|
|
|
247
469
|
/**
|
|
248
470
|
* Print corpus availability section.
|
|
471
|
+
*
|
|
472
|
+
* The atlas records corpora where its sources put them — OPUS parallel
|
|
473
|
+
* corpora, monolingual corpora, UD treebanks and speech under `resources`,
|
|
474
|
+
* wordlists under `lexicalResources.datasets`. The legacy `corpusAvailability`
|
|
475
|
+
* block is read too, for old-shape cards; the published projection ships it
|
|
476
|
+
* as `{}`, which once printed this heading over nothing.
|
|
249
477
|
*/
|
|
250
|
-
function renderCorpus(card) {
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
478
|
+
function renderCorpus(card, ctx) {
|
|
479
|
+
const code = pairCode(card);
|
|
480
|
+
section(ctx, 'Corpus Availability', () => {
|
|
481
|
+
const res = card.resources && typeof card.resources === 'object' ? card.resources : {};
|
|
482
|
+
const lex = card.lexicalResources && typeof card.lexicalResources === 'object' ? card.lexicalResources : {};
|
|
483
|
+
|
|
484
|
+
listField(ctx, 'Parallel (OPUS)', listOf(res.corpora).map((c) => {
|
|
485
|
+
const partners = listOf(c.topPartners).slice(0, 3)
|
|
486
|
+
.map((p) => `${p.code}:${p.alignmentPairs}`).join(', ');
|
|
487
|
+
const pairs = typeof c.alignmentPairsTotal === 'number'
|
|
488
|
+
? `${c.alignmentPairsTotal.toLocaleString()} aligned pairs` : 'size not published';
|
|
489
|
+
return `${c.corpus ?? c.corpusId ?? '?'} — ${pairs}${partners ? fmt.dim(` (top partners ${partners})`) : ''}`;
|
|
490
|
+
}));
|
|
491
|
+
listField(ctx, 'Monolingual', listOf(res.monolingualCorpora).map(toolLine));
|
|
492
|
+
listField(ctx, 'Treebanks (UD)', listOf(res.treebanks).map((t) => (
|
|
493
|
+
`${t.treebank ?? '?'}${typeof t.sentences === 'number' ? ` — ${t.sentences.toLocaleString()} sentences` : ''}${t.release ? fmt.dim(` (release ${t.release})`) : ''}`
|
|
494
|
+
)));
|
|
495
|
+
listField(ctx, 'Speech', listOf(res.speech).map((s) => (
|
|
496
|
+
`${s.dataset ?? '?'}${s.locale ? ` ${s.locale}` : ''}${typeof s.validatedHours === 'number' ? ` — ${s.validatedHours} validated hours` : ''}${s.release ? fmt.dim(` (${s.release})`) : ''}`
|
|
497
|
+
)));
|
|
498
|
+
listField(ctx, 'Wordlists', listOf(lex.datasets).map((d) => (
|
|
499
|
+
`${d.dataset ?? '?'}${typeof d.forms === 'number' ? ` — ${d.forms.toLocaleString()} forms` : ''}${d.release ? fmt.dim(` (${d.release})`) : ''}`
|
|
500
|
+
)));
|
|
501
|
+
|
|
502
|
+
const corpus = card.corpusAvailability && typeof card.corpusAvailability === 'object'
|
|
503
|
+
? card.corpusAvailability : {};
|
|
504
|
+
if (corpus.opus) {
|
|
505
|
+
const opusInfo = [];
|
|
506
|
+
if (corpus.opus.corpora) opusInfo.push(`${corpus.opus.corpora} corpora`);
|
|
507
|
+
if (corpus.opus.languagePairs) opusInfo.push(`${corpus.opus.languagePairs} lang pairs`);
|
|
508
|
+
if (corpus.opus.totalAlignmentPairs) opusInfo.push(`${corpus.opus.totalAlignmentPairs.toLocaleString()} alignment pairs`);
|
|
509
|
+
field(ctx, 'OPUS', opusInfo.join(', '));
|
|
510
|
+
if (corpus.opus.corpusNames && corpus.opus.corpusNames.length > 0) {
|
|
511
|
+
field(ctx, ' Top corpora', corpus.opus.corpusNames.slice(0, 5).join(', '));
|
|
512
|
+
}
|
|
264
513
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
514
|
+
if (corpus.lexibank) {
|
|
515
|
+
field(ctx, 'Lexibank', `${corpus.lexibank.datasets} datasets, ${corpus.lexibank.totalForms} forms`);
|
|
516
|
+
}
|
|
517
|
+
if (corpus.ud) {
|
|
518
|
+
field(ctx, 'Universal Deps', `${corpus.ud.treebanks} treebanks: ${corpus.ud.treebankNames?.join(', ') || ''}`);
|
|
519
|
+
}
|
|
520
|
+
if (corpus.asjpWordlists) field(ctx, 'ASJP', `${corpus.asjpWordlists} wordlist(s), ${corpus.asjpForms || '?'} forms`);
|
|
521
|
+
if (corpus.huggingFaceDatasets) field(ctx, 'HuggingFace', `${corpus.huggingFaceDatasets} datasets`);
|
|
522
|
+
if (corpus.unimorphParadigms) field(ctx, 'UniMorph', fmt.green('✓'));
|
|
523
|
+
if (corpus.wiktionaryStructuredDump) field(ctx, 'Wiktionary dump', fmt.green('✓'));
|
|
524
|
+
if (corpus.openMultilingualWordnet) field(ctx, 'Open Multilingual WN', fmt.green('✓'));
|
|
525
|
+
}, [
|
|
526
|
+
NOT_RECORDED,
|
|
527
|
+
`registered test sets for a pair: mt-eval corpora --source <src> --target ${code}`,
|
|
528
|
+
]);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** A named tool or archive: who publishes it, and under what licence. */
|
|
532
|
+
function toolLine(t) {
|
|
533
|
+
const lic = t.license
|
|
534
|
+
? `${t.license}${t.licenceEstablished === false ? ' (licence NOT established)' : ''}`
|
|
535
|
+
: 'licence not stated';
|
|
536
|
+
return `${t.name ?? '?'} — ${t.publisher ?? 'publisher not stated'}, ${lic}${t.archived ? ', archived' : ''}`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Print what exists for the language beyond corpora: dictionaries,
|
|
541
|
+
* morphological analyzers, keyboards, and how far it is documented.
|
|
542
|
+
* Existence only — a listing, never a quality claim.
|
|
543
|
+
*/
|
|
544
|
+
function renderResources(card, ctx) {
|
|
545
|
+
section(ctx, 'Language Resources', () => {
|
|
546
|
+
const res = card.resources && typeof card.resources === 'object' ? card.resources : {};
|
|
547
|
+
const lex = card.lexicalResources && typeof card.lexicalResources === 'object' ? card.lexicalResources : {};
|
|
548
|
+
listField(ctx, 'Dictionaries', listOf(lex.dictionaries).map((d) => (
|
|
549
|
+
`${toolLine(d)}${d.pairedWith ? fmt.dim(` (paired with ${d.pairedWith})`) : ''}`
|
|
550
|
+
)));
|
|
551
|
+
listField(ctx, 'Analyzers (FST)', listOf(res.fsts).map(toolLine));
|
|
552
|
+
listField(ctx, 'Keyboards', listOf(res.keyboards).map(toolLine));
|
|
553
|
+
if (card.documentation?.medLevel) {
|
|
554
|
+
const src = card._fieldSources?.['documentation.medLevel'];
|
|
555
|
+
const cited = Array.isArray(src) ? src.join(', ') : src;
|
|
556
|
+
field(ctx, 'Documentation level', `${card.documentation.medLevel}${cited ? fmt.dim(` [${cited}]`) : ''}`);
|
|
557
|
+
}
|
|
558
|
+
}, ['no dictionary, analyzer, keyboard or documentation level recorded on this card (unknown, not "none")']);
|
|
277
559
|
}
|
|
278
560
|
|
|
279
561
|
/**
|
|
280
562
|
* Print eval datasets section.
|
|
281
563
|
*/
|
|
282
|
-
function renderEval(card) {
|
|
564
|
+
function renderEval(card, ctx) {
|
|
283
565
|
if (!card.evalDatasets || card.evalDatasets.length === 0) return;
|
|
284
566
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
567
|
+
section(ctx, 'Eval Datasets', () => {
|
|
568
|
+
for (const ds of card.evalDatasets) {
|
|
569
|
+
ctx.out.push(` ${fmt.cyan('•')} ${ds}`);
|
|
570
|
+
}
|
|
571
|
+
if (card.omt1600) {
|
|
572
|
+
// The OMT-1600 paper publishes no per-language tier table, so `tier` is
|
|
573
|
+
// null on most covered languages. Print the clause only when there is a
|
|
574
|
+
// cited tier — never "tier null".
|
|
575
|
+
const tier = card.omt1600.tier ? ` — tier ${card.omt1600.tier}` : '';
|
|
576
|
+
field(ctx, 'OMT-1600', card.omt1600.covered ? `${fmt.green('covered')}${tier}` : fmt.dim('not covered'));
|
|
577
|
+
}
|
|
578
|
+
});
|
|
296
579
|
}
|
|
297
580
|
|
|
298
581
|
/**
|
|
299
582
|
* Print pipeline readiness section.
|
|
300
583
|
*/
|
|
301
|
-
function renderPipeline(card) {
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
584
|
+
function renderPipeline(card, ctx) {
|
|
585
|
+
const code = pairCode(card);
|
|
586
|
+
section(ctx, 'Pipeline Readiness', () => {
|
|
587
|
+
const pr = card.pipelineReadiness;
|
|
588
|
+
if (!pr || typeof pr !== 'object') return;
|
|
589
|
+
|
|
590
|
+
// Score bar — visual indicator of readiness
|
|
591
|
+
const score = typeof pr.score === 'number' ? pr.score : null;
|
|
592
|
+
if (score !== null) {
|
|
593
|
+
const barWidth = 20;
|
|
594
|
+
const filled = Math.round((score / 100) * barWidth);
|
|
595
|
+
const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
|
|
596
|
+
const colorFn = score >= 70 ? fmt.green : score >= 40 ? fmt.yellow : fmt.red;
|
|
597
|
+
field(ctx, 'Score', `${colorFn(bar)} ${score}/100 (${pr.tier || ''})`);
|
|
598
|
+
} else {
|
|
599
|
+
field(ctx, 'Tier', pr.tier);
|
|
600
|
+
}
|
|
318
601
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
602
|
+
// Component checklist
|
|
603
|
+
if (pr.components && typeof pr.components === 'object' && Object.keys(pr.components).length) {
|
|
604
|
+
const checks = Object.entries(pr.components)
|
|
605
|
+
.map(([key, val]) => `${val ? fmt.green('✓') : fmt.dim('✗')} ${key}`)
|
|
606
|
+
.join(' ');
|
|
607
|
+
ctx.out.push(` ${checks}`);
|
|
608
|
+
}
|
|
609
|
+
}, [
|
|
610
|
+
NOT_RECORDED,
|
|
611
|
+
`what you can run for a pair today: champollion network recommend <src> ${code}`,
|
|
612
|
+
]);
|
|
326
613
|
}
|
|
327
614
|
|
|
328
615
|
/**
|
|
329
616
|
* Print method support section.
|
|
330
617
|
*/
|
|
331
|
-
function renderMethods(card) {
|
|
332
|
-
const
|
|
333
|
-
if (!methods) return;
|
|
334
|
-
|
|
335
|
-
heading('Method Support');
|
|
336
|
-
|
|
618
|
+
function renderMethods(card, ctx) {
|
|
619
|
+
const code = pairCode(card);
|
|
337
620
|
const methodNames = {
|
|
338
621
|
googleTranslate: 'Google Translate',
|
|
339
622
|
deepl: 'DeepL',
|
|
@@ -344,51 +627,117 @@ function renderMethods(card) {
|
|
|
344
627
|
microsoft: 'Microsoft',
|
|
345
628
|
};
|
|
346
629
|
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
630
|
+
section(ctx, 'Method Support', () => {
|
|
631
|
+
const methods = card.methodSupport;
|
|
632
|
+
if (!methods || typeof methods !== 'object') return;
|
|
633
|
+
for (const [key, info] of Object.entries(methods)) {
|
|
634
|
+
// Only a yes/no listing entry is a method. The atlas's evidence shape
|
|
635
|
+
// ({total, byTier, named}) reaching here unflattened must not print its
|
|
636
|
+
// bookkeeping keys as three "unsupported" engines.
|
|
637
|
+
if (!info || typeof info !== 'object' || !('supported' in info)) continue;
|
|
638
|
+
const label = methodNames[key] || key;
|
|
639
|
+
const badge = info.supported ? fmt.green('✓ supported') : fmt.dim('✗ unsupported');
|
|
640
|
+
let extra = '';
|
|
641
|
+
if (info.formality) extra += ` ${fmt.cyan('(formality)')}`;
|
|
642
|
+
if (info.code) extra += ` ${fmt.dim(`[${info.code}]`)}`;
|
|
643
|
+
if (info.verifiedDate) extra += ` ${fmt.dim(`verified ${info.verifiedDate}`)}`;
|
|
644
|
+
field(ctx, label, `${badge}${extra}`);
|
|
645
|
+
}
|
|
646
|
+
}, [
|
|
647
|
+
NOT_RECORDED,
|
|
648
|
+
`engines you can run for a pair, with the evidence for each: champollion network recommend <src> ${code}`,
|
|
649
|
+
]);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Print every disputed attribution envelope no section above printed.
|
|
654
|
+
*
|
|
655
|
+
* The sections name the fields they render; a field the atlas attributes
|
|
656
|
+
* that no section knows (politenessDistinction, bcp47FullTag, …) would
|
|
657
|
+
* otherwise carry its disagreement in silence. Walking the card finds them,
|
|
658
|
+
* including ones added after this file was written.
|
|
659
|
+
*/
|
|
660
|
+
function renderOtherDisputes(card, ctx) {
|
|
661
|
+
const byPath = new Map();
|
|
662
|
+
for (const source of [ctx.rawCard, card]) {
|
|
663
|
+
if (!source) continue;
|
|
664
|
+
for (const { path, value } of attributedFields(source)) {
|
|
665
|
+
if (!byPath.has(path)) byPath.set(path, value);
|
|
666
|
+
}
|
|
356
667
|
}
|
|
668
|
+
const rest = [...byPath].filter(([path, value]) => isDisputed(value) && !ctx.rendered.has(path));
|
|
669
|
+
if (!rest.length) return;
|
|
670
|
+
|
|
671
|
+
section(ctx, 'Other Fields Where Sources Differ', () => {
|
|
672
|
+
for (const [path, value] of rest) {
|
|
673
|
+
const label = path.split('.').map((seg) => labelFor(seg)).join(' › ');
|
|
674
|
+
field(ctx, label, value);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
357
677
|
}
|
|
358
678
|
|
|
359
679
|
/**
|
|
360
680
|
* Print data sources count and cultural aphorism.
|
|
361
681
|
*/
|
|
362
|
-
function renderFooter(card) {
|
|
682
|
+
function renderFooter(card, ctx) {
|
|
363
683
|
// Data sources count
|
|
364
684
|
if (card.dataSources && card.dataSources.length > 0) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
685
|
+
section(ctx, 'Data Sources', () => {
|
|
686
|
+
field(ctx, 'Count', `${card.dataSources.length} sources`);
|
|
687
|
+
// Show first few and last few if there are many
|
|
688
|
+
if (card.dataSources.length <= 8) {
|
|
689
|
+
field(ctx, 'Sources', card.dataSources.join(', '));
|
|
690
|
+
} else {
|
|
691
|
+
const preview = [...card.dataSources.slice(0, 5), `… +${card.dataSources.length - 5} more`];
|
|
692
|
+
field(ctx, 'Sources', preview.join(', '));
|
|
693
|
+
}
|
|
694
|
+
});
|
|
374
695
|
}
|
|
375
696
|
|
|
376
697
|
// Cultural aphorism — a nice touch at the bottom
|
|
377
|
-
if (card.culturalAphorism) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
698
|
+
if (card.culturalAphorism?.text) {
|
|
699
|
+
section(ctx, 'Cultural Aphorism', () => {
|
|
700
|
+
ctx.out.push(` ${fmt.magenta(`"${card.culturalAphorism.text}"`)}`);
|
|
701
|
+
if (card.culturalAphorism.translation) {
|
|
702
|
+
ctx.out.push(` ${fmt.dim(`— "${card.culturalAphorism.translation}"`)}`);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
383
705
|
}
|
|
384
706
|
|
|
385
707
|
// Notes
|
|
386
708
|
if (card.notes) {
|
|
387
|
-
|
|
388
|
-
|
|
709
|
+
ctx.out.push('');
|
|
710
|
+
ctx.out.push(` ${fmt.dim('Note:')} ${fmt.dim(card.notes)}`);
|
|
389
711
|
}
|
|
390
712
|
|
|
391
|
-
|
|
713
|
+
ctx.out.push('');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Render a card as the terminal text `champollion network card` prints.
|
|
718
|
+
*
|
|
719
|
+
* @param {object} card the registry's view (getLanguageCard) — normalized
|
|
720
|
+
* @param {object} [opts]
|
|
721
|
+
* @param {object|null} [opts.rawCard] the on-disk atlas card when readable
|
|
722
|
+
* (repo mode). The registry view flattens `name` to a label; the raw card
|
|
723
|
+
* still holds its envelope, so a disputed name can be shown in full.
|
|
724
|
+
* @returns {string}
|
|
725
|
+
*/
|
|
726
|
+
function renderCard(card, { rawCard = null } = {}) {
|
|
727
|
+
const ctx = { out: [], rendered: new Set(), rawCard };
|
|
728
|
+
renderHeader(card, ctx);
|
|
729
|
+
renderClassification(card, ctx);
|
|
730
|
+
renderSpeakers(card, ctx);
|
|
731
|
+
renderEndangerment(card, ctx);
|
|
732
|
+
renderTypology(card, ctx);
|
|
733
|
+
renderCorpus(card, ctx);
|
|
734
|
+
renderResources(card, ctx);
|
|
735
|
+
renderEval(card, ctx);
|
|
736
|
+
renderPipeline(card, ctx);
|
|
737
|
+
renderMethods(card, ctx);
|
|
738
|
+
renderOtherDisputes(card, ctx);
|
|
739
|
+
renderFooter(card, ctx);
|
|
740
|
+
return ctx.out.join('\n');
|
|
392
741
|
}
|
|
393
742
|
|
|
394
743
|
// -----------------------------------------------------------------
|
|
@@ -407,12 +756,12 @@ async function run(args, cwd) {
|
|
|
407
756
|
if (!code) {
|
|
408
757
|
console.error('[ERR] Missing language code argument.');
|
|
409
758
|
console.error('');
|
|
410
|
-
console.error(' Usage: champollion card <code> [--json]');
|
|
759
|
+
console.error(' Usage: champollion network card <code> [--json]');
|
|
411
760
|
console.error('');
|
|
412
761
|
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');
|
|
762
|
+
console.error(' champollion network card crk # Plains Cree');
|
|
763
|
+
console.error(' champollion network card spa # Spanish');
|
|
764
|
+
console.error(' champollion network card cmn --json # Raw JSON');
|
|
416
765
|
console.error('');
|
|
417
766
|
return 1;
|
|
418
767
|
}
|
|
@@ -431,24 +780,32 @@ async function run(args, cwd) {
|
|
|
431
780
|
return 1;
|
|
432
781
|
}
|
|
433
782
|
|
|
783
|
+
// ── Locale fell back to its language: say so ──
|
|
784
|
+
// Packaged installs carry only the 840 bundled locale deltas, and no
|
|
785
|
+
// locale code is ever published as a trading card, so a long-tail
|
|
786
|
+
// locale is served by its base language's card. Print the substitution
|
|
787
|
+
// rather than passing the language off as the locale.
|
|
788
|
+
if (!args.json && code.includes('-') && resolvedCode !== code) {
|
|
789
|
+
console.error(
|
|
790
|
+
`[NOTE] No locale card for "${code}" — showing the base language card "${resolvedCode}".`,
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
434
794
|
// ── --json mode: output raw JSON and exit ──
|
|
435
795
|
if (args.json) {
|
|
436
796
|
console.log(JSON.stringify(card, null, 2));
|
|
437
797
|
return 0;
|
|
438
798
|
}
|
|
439
799
|
|
|
440
|
-
// ── Pretty-print mode
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
renderPipeline(card);
|
|
448
|
-
renderMethods(card);
|
|
449
|
-
renderFooter(card);
|
|
800
|
+
// ── Pretty-print mode ──
|
|
801
|
+
// The on-disk atlas card (repo mode only; null in an npm install, whose
|
|
802
|
+
// bundled and fetched cards are already the flattened view) supplies the
|
|
803
|
+
// envelopes the registry view flattened, through the reader — never a
|
|
804
|
+
// private JSON.parse.
|
|
805
|
+
const rawCard = readCard(card.code ?? resolvedCode);
|
|
806
|
+
console.log(renderCard(card, { rawCard }));
|
|
450
807
|
|
|
451
808
|
return 0;
|
|
452
809
|
}
|
|
453
810
|
|
|
454
|
-
export { run };
|
|
811
|
+
export { run, renderCard };
|