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,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote card source — read-only access to the production Supabase
|
|
3
|
+
* trading-card tables, and reconstruction of language cards from them.
|
|
4
|
+
*
|
|
5
|
+
* TABLES (mt-eval-arena/supabase/migrations/012_create_trading_cards.sql):
|
|
6
|
+
* trading_card_index — one compact row per language (grid fields),
|
|
7
|
+
* per-language updated_at
|
|
8
|
+
* trading_card_detail — { code, detail JSONB, updated_at }; detail is
|
|
9
|
+
* the pre-resolved (extends already merged)
|
|
10
|
+
* card projection built by
|
|
11
|
+
* mt-eval-arena/scripts/build-trading-card-data.mjs
|
|
12
|
+
*
|
|
13
|
+
* Access is anon-key, SELECT-only (enforced by RLS). This module never
|
|
14
|
+
* writes to Supabase.
|
|
15
|
+
*
|
|
16
|
+
* RECONSTRUCTION FIDELITY: the detail blob carries most reference
|
|
17
|
+
* fields under their card names (classification, vitality, formality,
|
|
18
|
+
* methodSupport, …) but NOT the runtime-only fields `registers`,
|
|
19
|
+
* `rules`, `gender`, or `aliases`:
|
|
20
|
+
* - registers are re-derived from `formality` with the same templates
|
|
21
|
+
* the card build uses (scripts/derive-registers-from-formality.mjs)
|
|
22
|
+
* - rules/gender stay null — all consumers treat them as optional
|
|
23
|
+
* (lib/types.js documents rules as object|null)
|
|
24
|
+
* - aliases come from the bundled manifest (shared/cards-fallback.json)
|
|
25
|
+
* Languages with hand-curated registers ship in the bundled fallback
|
|
26
|
+
* set, so reconstruction only ever serves the long tail.
|
|
27
|
+
*
|
|
28
|
+
* Shared by the CLI's lazy card loader (lib/registers.js) and the
|
|
29
|
+
* website data build (cli/website/scripts/build-data-from-supabase.mjs).
|
|
30
|
+
* Zero dependencies — global fetch (Node >= 20.11).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { SUPABASE_URL, SUPABASE_ANON_KEY, isValidCardCode } from './env.js';
|
|
34
|
+
|
|
35
|
+
const REST_BASE = `${SUPABASE_URL}/rest/v1`;
|
|
36
|
+
|
|
37
|
+
/** PostgREST caps responses at 1000 rows per request on Supabase. */
|
|
38
|
+
const MAX_PAGE = 1000;
|
|
39
|
+
|
|
40
|
+
function headers(extra = {}) {
|
|
41
|
+
return {
|
|
42
|
+
apikey: SUPABASE_ANON_KEY,
|
|
43
|
+
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
|
|
44
|
+
...extra,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* GET a REST URL and parse JSON, with bounded retries on network
|
|
50
|
+
* errors and 5xx. 4xx responses throw immediately (they won't heal).
|
|
51
|
+
*/
|
|
52
|
+
export async function fetchJson(url, { timeoutMs = 15000, retries = 2, accept } = {}) {
|
|
53
|
+
let lastErr;
|
|
54
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(url, {
|
|
57
|
+
headers: headers(accept ? { Accept: accept } : {}),
|
|
58
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
59
|
+
});
|
|
60
|
+
if (res.ok) return await res.json();
|
|
61
|
+
const body = await res.text().catch(() => '');
|
|
62
|
+
const err = new Error(`Supabase request failed: HTTP ${res.status} ${body.slice(0, 200)}`);
|
|
63
|
+
err.status = res.status;
|
|
64
|
+
if (res.status >= 400 && res.status < 500) throw err;
|
|
65
|
+
lastErr = err;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err.status >= 400 && err.status < 500) throw err;
|
|
68
|
+
lastErr = err;
|
|
69
|
+
}
|
|
70
|
+
if (attempt < retries) {
|
|
71
|
+
await new Promise(r => setTimeout(r, 500 * (attempt + 1)));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
throw lastErr;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Fetch rows from trading_card_index, paginating until exhausted.
|
|
79
|
+
*
|
|
80
|
+
* @param {object} [opts]
|
|
81
|
+
* @param {string} [opts.select] - PostgREST select list (default: all columns)
|
|
82
|
+
* @param {string} [opts.since] - ISO timestamp; only rows with updated_at > since
|
|
83
|
+
* @param {number} [opts.pageSize]
|
|
84
|
+
* @param {number} [opts.timeoutMs]
|
|
85
|
+
* @returns {Promise<object[]>} snake_case rows
|
|
86
|
+
*/
|
|
87
|
+
export async function fetchIndexRows({ select = '*', since, pageSize = MAX_PAGE, timeoutMs = 30000 } = {}) {
|
|
88
|
+
const rows = [];
|
|
89
|
+
const size = Math.min(pageSize, MAX_PAGE);
|
|
90
|
+
for (let offset = 0; ; offset += size) {
|
|
91
|
+
const params = new URLSearchParams({
|
|
92
|
+
select,
|
|
93
|
+
order: 'code.asc',
|
|
94
|
+
limit: String(size),
|
|
95
|
+
offset: String(offset),
|
|
96
|
+
});
|
|
97
|
+
if (since) params.set('updated_at', `gt.${since}`);
|
|
98
|
+
const page = await fetchJson(`${REST_BASE}/trading_card_index?${params}`, { timeoutMs });
|
|
99
|
+
rows.push(...page);
|
|
100
|
+
if (page.length < size) break;
|
|
101
|
+
}
|
|
102
|
+
return rows;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Fetch one index row by code, or null if the language isn't in prod. */
|
|
106
|
+
export async function fetchIndexRow(code, { timeoutMs = 10000 } = {}) {
|
|
107
|
+
if (!isValidCardCode(code)) return null;
|
|
108
|
+
const params = new URLSearchParams({ select: '*', code: `eq.${code}`, limit: '1' });
|
|
109
|
+
const rows = await fetchJson(`${REST_BASE}/trading_card_index?${params}`, { timeoutMs });
|
|
110
|
+
return rows[0] || null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Fetch one detail row ({ code, detail, updated_at }) by code, or null. */
|
|
114
|
+
export async function fetchDetailRow(code, { timeoutMs = 10000 } = {}) {
|
|
115
|
+
if (!isValidCardCode(code)) return null;
|
|
116
|
+
const params = new URLSearchParams({
|
|
117
|
+
select: 'code,detail,updated_at',
|
|
118
|
+
code: `eq.${code}`,
|
|
119
|
+
limit: '1',
|
|
120
|
+
});
|
|
121
|
+
const rows = await fetchJson(`${REST_BASE}/trading_card_detail?${params}`, { timeoutMs });
|
|
122
|
+
return rows[0] || null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Fetch every detail row, paginating in modest pages (rows average
|
|
127
|
+
* ~9 KB; large pages risk statement timeouts). Used by website data
|
|
128
|
+
* builds, not by the CLI hot path.
|
|
129
|
+
*
|
|
130
|
+
* @param {object} [opts]
|
|
131
|
+
* @param {number} [opts.pageSize]
|
|
132
|
+
* @param {(rows: object[], offset: number) => void} [opts.onPage] - streaming hook
|
|
133
|
+
*/
|
|
134
|
+
export async function fetchAllDetailRows({ pageSize = 200, timeoutMs = 60000, onPage } = {}) {
|
|
135
|
+
const all = [];
|
|
136
|
+
for (let offset = 0; ; offset += pageSize) {
|
|
137
|
+
const params = new URLSearchParams({
|
|
138
|
+
select: 'code,detail,updated_at',
|
|
139
|
+
order: 'code.asc',
|
|
140
|
+
limit: String(pageSize),
|
|
141
|
+
offset: String(offset),
|
|
142
|
+
});
|
|
143
|
+
const page = await fetchJson(`${REST_BASE}/trading_card_detail?${params}`, { timeoutMs });
|
|
144
|
+
if (onPage) onPage(page, offset);
|
|
145
|
+
else all.push(...page);
|
|
146
|
+
if (page.length < pageSize) break;
|
|
147
|
+
}
|
|
148
|
+
return all;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// -----------------------------------------------------------------
|
|
152
|
+
// Registers derivation — ported verbatim from
|
|
153
|
+
// scripts/derive-registers-from-formality.mjs so cards reconstructed
|
|
154
|
+
// from Supabase carry the same derived presets as their on-disk
|
|
155
|
+
// counterparts. Keep the two in sync if templates ever change.
|
|
156
|
+
// -----------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
const REGISTER_TEMPLATES = {
|
|
159
|
+
'T-V': {
|
|
160
|
+
formal: {
|
|
161
|
+
label: 'Formal (V-form)',
|
|
162
|
+
description: 'Polite/formal address using V-form pronouns (e.g., vous, Sie, usted, вы). Default for software UI, official communication, and addressing strangers.',
|
|
163
|
+
prompt: 'Use formal/polite address forms (V-form pronouns). Write in a professional, respectful tone appropriate for software UI and official communication.',
|
|
164
|
+
isDefault: true,
|
|
165
|
+
},
|
|
166
|
+
informal: {
|
|
167
|
+
label: 'Informal (T-form)',
|
|
168
|
+
description: 'Familiar/informal address using T-form pronouns (e.g., tu, du, tú, ты). Used with close friends, family, children, and peers of similar age.',
|
|
169
|
+
prompt: 'Use informal/familiar address forms (T-form pronouns). Write in a casual, friendly tone appropriate for peer-to-peer communication.',
|
|
170
|
+
isDefault: false,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
'speech-levels': {
|
|
174
|
+
formal: {
|
|
175
|
+
label: 'Formal/Honorific',
|
|
176
|
+
description: 'Highest politeness level with dedicated verb morphology and honorific vocabulary. Used in formal contexts, with elders, officials, and strangers.',
|
|
177
|
+
prompt: 'Use the highest politeness level with honorific verb forms and respectful vocabulary. Address the reader with maximum deference.',
|
|
178
|
+
isDefault: false,
|
|
179
|
+
},
|
|
180
|
+
polite: {
|
|
181
|
+
label: 'Polite/Standard',
|
|
182
|
+
description: 'Standard polite register. Appropriate for most software UI, everyday formal interactions, and default translation output.',
|
|
183
|
+
prompt: 'Use standard polite register. Professional and respectful without excessive formality. Appropriate for software UI and general communication.',
|
|
184
|
+
isDefault: true,
|
|
185
|
+
},
|
|
186
|
+
informal: {
|
|
187
|
+
label: 'Informal/Casual',
|
|
188
|
+
description: 'Casual register used among close friends, peers, and in informal settings. Not appropriate for software UI or official content.',
|
|
189
|
+
prompt: 'Use casual/informal register. Relaxed tone appropriate for friends and peers. Avoid honorifics and formal verb endings.',
|
|
190
|
+
isDefault: false,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
'avoidance': {
|
|
194
|
+
respectful: {
|
|
195
|
+
label: 'Respectful/Indirect',
|
|
196
|
+
description: 'Address through indirect reference, titles, kinship terms, or circumlocution. Primary mode for respectful communication.',
|
|
197
|
+
prompt: 'Use indirect address strategies: titles, kinship terms, or circumlocution rather than direct pronouns. Maintain respectful distance.',
|
|
198
|
+
isDefault: true,
|
|
199
|
+
},
|
|
200
|
+
neutral: {
|
|
201
|
+
label: 'Neutral/Direct',
|
|
202
|
+
description: 'Direct address without avoidance strategies. Used in informal or intimate contexts where indirectness is unnecessary.',
|
|
203
|
+
prompt: 'Use direct address without avoidance strategies. Straightforward and clear communication.',
|
|
204
|
+
isDefault: false,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
'none': {
|
|
208
|
+
standard: {
|
|
209
|
+
label: 'Standard',
|
|
210
|
+
description: 'No grammaticalized politeness distinctions. A single register serves all social contexts, though lexical choices may vary by formality.',
|
|
211
|
+
prompt: 'Use standard register. This language does not grammaticalize politeness distinctions. Write clearly and naturally.',
|
|
212
|
+
isDefault: true,
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
'politeness-present': {
|
|
216
|
+
formal: {
|
|
217
|
+
label: 'Formal/Polite',
|
|
218
|
+
description: 'Polite register using politeness-marked pronoun forms. Default for software UI and formal contexts.',
|
|
219
|
+
prompt: 'Use polite/formal pronoun forms and politeness markers. Professional tone appropriate for software UI and formal contexts.',
|
|
220
|
+
isDefault: true,
|
|
221
|
+
},
|
|
222
|
+
informal: {
|
|
223
|
+
label: 'Informal/Familiar',
|
|
224
|
+
description: 'Informal register using unmarked/familiar pronoun forms. Used with close relations.',
|
|
225
|
+
prompt: 'Use unmarked/familiar pronoun forms. Casual tone appropriate for friends and family.',
|
|
226
|
+
isDefault: false,
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Derive a registers block from a formality field, mirroring the
|
|
233
|
+
* build-time enrichment. Returns { registers, defaultKey } or null
|
|
234
|
+
* when the formality system is unknown.
|
|
235
|
+
*/
|
|
236
|
+
export function deriveRegistersFromFormality(formality) {
|
|
237
|
+
if (!formality || !formality.system) return null;
|
|
238
|
+
const template = REGISTER_TEMPLATES[formality.system];
|
|
239
|
+
if (!template) return null;
|
|
240
|
+
|
|
241
|
+
const registers = {};
|
|
242
|
+
let defaultKey = null;
|
|
243
|
+
for (const [key, val] of Object.entries(template)) {
|
|
244
|
+
registers[key] = {
|
|
245
|
+
...val,
|
|
246
|
+
source: `derived-from-formality (${formality.source || 'unknown'})`,
|
|
247
|
+
};
|
|
248
|
+
if (val.isDefault) defaultKey = key;
|
|
249
|
+
}
|
|
250
|
+
return { registers, defaultKey };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// -----------------------------------------------------------------
|
|
254
|
+
// Card reconstruction
|
|
255
|
+
// -----------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Detail-blob fields that map 1:1 onto card fields. Display-only
|
|
259
|
+
* assemblies whose shape differs from the raw cards (vocabulary,
|
|
260
|
+
* typology, phonology, statSources, provenance, …) are deliberately
|
|
261
|
+
* not mapped — fabricating card fields with mismatched shapes would
|
|
262
|
+
* be worse than omitting them.
|
|
263
|
+
*/
|
|
264
|
+
const DETAIL_PASSTHROUGH = [
|
|
265
|
+
'classification',
|
|
266
|
+
'vitality',
|
|
267
|
+
'speakerEstimates',
|
|
268
|
+
'linguisticChallenges',
|
|
269
|
+
'contactInfluences',
|
|
270
|
+
'formality',
|
|
271
|
+
'methodSupport',
|
|
272
|
+
'resources',
|
|
273
|
+
'evalDatasets',
|
|
274
|
+
'pipelineReadiness',
|
|
275
|
+
'digitalPresence',
|
|
276
|
+
'corpusAvailability',
|
|
277
|
+
'databaseCoverage',
|
|
278
|
+
'omt1600',
|
|
279
|
+
'regions',
|
|
280
|
+
'countries',
|
|
281
|
+
'coordinates',
|
|
282
|
+
'orthographicStatus',
|
|
283
|
+
'notes',
|
|
284
|
+
'encyclopedic',
|
|
285
|
+
'experts',
|
|
286
|
+
'alternateNames',
|
|
287
|
+
'modality',
|
|
288
|
+
'isoType',
|
|
289
|
+
'isoScope',
|
|
290
|
+
'macrolanguage',
|
|
291
|
+
'members',
|
|
292
|
+
'taxonomyNotes',
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Build a language card from remote rows. The result is card-shaped
|
|
297
|
+
* (same field names as shared/language-cards/*.json) and ready for
|
|
298
|
+
* the registry — it has no `extends` because the upload pipeline
|
|
299
|
+
* resolves inheritance before publishing.
|
|
300
|
+
*
|
|
301
|
+
* @param {object} indexRow - snake_case trading_card_index row
|
|
302
|
+
* @param {object|null} detailRow - { detail, updated_at } or null
|
|
303
|
+
* @param {object} [opts]
|
|
304
|
+
* @param {string[]} [opts.aliases] - aliases from the bundled manifest
|
|
305
|
+
* @returns {object} reconstructed card
|
|
306
|
+
*/
|
|
307
|
+
export function buildCardFromRemote(indexRow, detailRow, { aliases } = {}) {
|
|
308
|
+
const detail = detailRow?.detail || {};
|
|
309
|
+
const code = indexRow.code;
|
|
310
|
+
|
|
311
|
+
const card = {
|
|
312
|
+
code,
|
|
313
|
+
name: indexRow.name || code,
|
|
314
|
+
nativeName: indexRow.native_name ?? null,
|
|
315
|
+
iso639_3: /^[a-z]{3}$/.test(code) ? code : null,
|
|
316
|
+
iso639_1: null,
|
|
317
|
+
bcp47: code,
|
|
318
|
+
glottocode: detail.glottocode ?? indexRow.glottocode ?? null,
|
|
319
|
+
isIsolate: indexRow.is_isolate ?? false,
|
|
320
|
+
macroarea: indexRow.macroarea ?? null,
|
|
321
|
+
// Taxonomy badging — carried on the index row since migration 056.
|
|
322
|
+
// The detail blob's values (DETAIL_PASSTHROUGH below) override these
|
|
323
|
+
// when present; rows uploaded before the migration leave them null.
|
|
324
|
+
modality: indexRow.modality ?? null,
|
|
325
|
+
isoType: indexRow.iso_type ?? null,
|
|
326
|
+
isoScope: indexRow.iso_scope ?? null,
|
|
327
|
+
macrolanguage: indexRow.macrolanguage ?? null,
|
|
328
|
+
script: indexRow.script ?? null,
|
|
329
|
+
scripts: indexRow.scripts ?? null,
|
|
330
|
+
dir: indexRow.dir || 'ltr',
|
|
331
|
+
dialectCount: indexRow.dialect_count ?? null,
|
|
332
|
+
culturalAphorism: indexRow.cultural_aphorism ?? null,
|
|
333
|
+
registers: null,
|
|
334
|
+
rules: null,
|
|
335
|
+
gender: null,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
if (Array.isArray(aliases) && aliases.length > 0) card.aliases = aliases;
|
|
339
|
+
|
|
340
|
+
for (const field of DETAIL_PASSTHROUGH) {
|
|
341
|
+
if (detail[field] !== undefined && detail[field] !== null) {
|
|
342
|
+
card[field] = detail[field];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Re-derive registers the same way the card build does, so
|
|
347
|
+
// getRegister()/getRegisterPresets() behave identically to the
|
|
348
|
+
// on-disk stub cards.
|
|
349
|
+
const derived = deriveRegistersFromFormality(card.formality);
|
|
350
|
+
if (derived) {
|
|
351
|
+
card.registers = derived.registers;
|
|
352
|
+
if (derived.defaultKey && !card.formality.default) {
|
|
353
|
+
card.formality = { ...card.formality, default: derived.defaultKey };
|
|
354
|
+
}
|
|
355
|
+
card._fieldSources = {
|
|
356
|
+
registers: `derived-from-formality (${card.formality.source || 'unknown'})`,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
card._remote = {
|
|
361
|
+
source: 'supabase',
|
|
362
|
+
updatedAt: detailRow?.updated_at || indexRow.updated_at || null,
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
return card;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Fetch and reconstruct one card.
|
|
370
|
+
*
|
|
371
|
+
* @returns {Promise<{card: object, updatedAt: string|null}|{missing: true}>}
|
|
372
|
+
* missing:true when the language doesn't exist in prod (definitive
|
|
373
|
+
* 404-equivalent — callers may tombstone it). Network/server errors
|
|
374
|
+
* throw instead.
|
|
375
|
+
*/
|
|
376
|
+
export async function fetchRemoteCard(code, { aliases, timeoutMs = 10000 } = {}) {
|
|
377
|
+
if (!isValidCardCode(code)) return { missing: true };
|
|
378
|
+
const [indexRow, detailRow] = await Promise.all([
|
|
379
|
+
fetchIndexRow(code, { timeoutMs }),
|
|
380
|
+
fetchDetailRow(code, { timeoutMs }),
|
|
381
|
+
]);
|
|
382
|
+
if (!indexRow) return { missing: true };
|
|
383
|
+
const card = buildCardFromRemote(indexRow, detailRow, { aliases });
|
|
384
|
+
return { card, updatedAt: card._remote.updatedAt };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export { SUPABASE_URL, SUPABASE_ANON_KEY };
|