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
package/lib/registers.js
ADDED
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language card registry — structured language metadata and register presets.
|
|
3
|
+
*
|
|
4
|
+
* ARCHITECTURE (v7 — Dynamic Cards):
|
|
5
|
+
*
|
|
6
|
+
* Each language has a single card containing ALL metadata: runtime
|
|
7
|
+
* fields (code, registers, formality, methodSupport, rules) AND
|
|
8
|
+
* reference data (linguisticChallenges, encyclopedic, resources,
|
|
9
|
+
* typology, vitality). Cards come from three tiers:
|
|
10
|
+
*
|
|
11
|
+
* 1. REPO MODE — shared/language-cards/<code>.json exists (git
|
|
12
|
+
* checkout): every card is loaded eagerly at module init, exactly
|
|
13
|
+
* as in v6. No network is ever touched. Tests run in this mode.
|
|
14
|
+
*
|
|
15
|
+
* 2. PACKAGED MODE — the npm tarball excludes the ~72 MB card
|
|
16
|
+
* directory and ships shared/cards-fallback.json instead (core
|
|
17
|
+
* languages + all genera/ parents + a full code/alias manifest,
|
|
18
|
+
* built by scripts/build-cards-fallback.mjs). Core languages work
|
|
19
|
+
* offline. Long-tail lookups are served from the per-user cache
|
|
20
|
+
* (~/.champollion/cards/) and, on a miss, fetched synchronously
|
|
21
|
+
* from the production Supabase trading-card tables (read-only
|
|
22
|
+
* anon key) via a child process — see lib/cards/. Cached entries
|
|
23
|
+
* are invalidated by per-language updated_at (lib/cards/refresh.js,
|
|
24
|
+
* run by bin/cli.js at most once per day). Set CHAMPOLLION_OFFLINE=1
|
|
25
|
+
* to disable all network use.
|
|
26
|
+
*
|
|
27
|
+
* 3. Genus/family/macrolanguage abstract cards ship in both modes;
|
|
28
|
+
* language cards use "extends" to inherit from them, including
|
|
29
|
+
* cards loaded from the cache or the network.
|
|
30
|
+
*
|
|
31
|
+
* ADDING A NEW LANGUAGE:
|
|
32
|
+
* 1. Create shared/language-cards/<code>.json (all fields)
|
|
33
|
+
* 2. Research the language's formality system and write register presets
|
|
34
|
+
* 3. Validate against shared/schemas/language-card.schema.json
|
|
35
|
+
* 4. Add eval dataset references if available
|
|
36
|
+
* 5. Regenerate the bundle: node scripts/build-cards-fallback.mjs
|
|
37
|
+
*
|
|
38
|
+
* BACKWARD COMPATIBILITY:
|
|
39
|
+
* DEFAULT_REGISTERS is still exported as a backward-compatible proxy.
|
|
40
|
+
* It returns the same shape as the old flat dictionary ({ name, register,
|
|
41
|
+
* dir?, scripts? }) so existing consumers don't break during migration.
|
|
42
|
+
* New code should use getLanguageCard() and getRegister() instead.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import fs from 'node:fs';
|
|
46
|
+
import path from 'node:path';
|
|
47
|
+
import { execFileSync } from 'node:child_process';
|
|
48
|
+
|
|
49
|
+
import { CARDS_DIR, FALLBACK_FILE, OFFLINE, isValidCardCode } from './cards/env.js';
|
|
50
|
+
import {
|
|
51
|
+
readCachedCard,
|
|
52
|
+
writeCachedCard,
|
|
53
|
+
listCachedCodes,
|
|
54
|
+
readState,
|
|
55
|
+
inBackoff,
|
|
56
|
+
isTombstoned,
|
|
57
|
+
tombstone,
|
|
58
|
+
noteNetworkFailure,
|
|
59
|
+
} from './cards/cache.js';
|
|
60
|
+
import { fetchRemoteCard } from './cards/remote.js';
|
|
61
|
+
import { normalizeCard } from './cards/reader.js';
|
|
62
|
+
|
|
63
|
+
// -----------------------------------------------------------------
|
|
64
|
+
// Card loading — full directory (repo) or bundled fallback (package)
|
|
65
|
+
// -----------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/** Child script used for synchronous fetches (see _tryFetchRemoteSync). */
|
|
68
|
+
const FETCH_CHILD = path.join(import.meta.dirname, 'cards', 'fetch-card-child.js');
|
|
69
|
+
|
|
70
|
+
// NOTE: The former REFERENCE_DIR and reference tier (shared/language-reference/)
|
|
71
|
+
// has been merged into the runtime cards (v6 architecture). All data is now
|
|
72
|
+
// in shared/language-cards/. The REFERENCE_DIR export is kept temporarily
|
|
73
|
+
// for backward compatibility but points nowhere.
|
|
74
|
+
const REFERENCE_DIR = null;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* In-memory registry of all loaded language cards.
|
|
78
|
+
* Keyed by primary locale code (e.g., 'fr', 'ko', 'x-pirate').
|
|
79
|
+
* @type {Map<string, object>}
|
|
80
|
+
*/
|
|
81
|
+
const _cards = new Map();
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Registry of parent/abstract cards (genus and family cards in genera/).
|
|
85
|
+
* Keyed by code (e.g., 'family-algonquian', 'genus-cree').
|
|
86
|
+
* @type {Map<string, object>}
|
|
87
|
+
*/
|
|
88
|
+
const _parentCards = new Map();
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Alias map — alternative locale codes that resolve to a primary code.
|
|
92
|
+
* e.g., 'no' → 'nb', 'iw' → 'he', 'zh-CN' → 'zh', 'fil' → 'tl'
|
|
93
|
+
* Built from the `aliases` field in each language card.
|
|
94
|
+
* @type {Map<string, string>}
|
|
95
|
+
*/
|
|
96
|
+
const _aliases = new Map();
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Manifest of every concrete card code in the catalogue (packaged
|
|
100
|
+
* mode only — empty in repo mode). Values: { n: name, a?: aliases,
|
|
101
|
+
* d?: dir }. Lets resolveCode() recognize the full catalogue without
|
|
102
|
+
* materializing it, and tells the fetch path which codes exist.
|
|
103
|
+
* @type {Map<string, object>}
|
|
104
|
+
*/
|
|
105
|
+
const _manifest = new Map();
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* How cards were loaded:
|
|
109
|
+
* 'repo' — full shared/language-cards/ directory
|
|
110
|
+
* 'packaged' — bundled fallback + cache + remote fetch
|
|
111
|
+
* 'none' — neither source available (registry empty)
|
|
112
|
+
* @type {'repo'|'packaged'|'none'}
|
|
113
|
+
*/
|
|
114
|
+
let _mode = 'none';
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Codes already attempted (hit or miss) against cache/network in this
|
|
118
|
+
* process — prevents repeated child spawns for the same code.
|
|
119
|
+
* @type {Set<string>}
|
|
120
|
+
*/
|
|
121
|
+
const _fetchAttempted = new Set();
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Register a concrete card into the registry (shared by all tiers).
|
|
125
|
+
*/
|
|
126
|
+
function _registerCard(card) {
|
|
127
|
+
_cards.set(card.code, card);
|
|
128
|
+
// `codeAliases` — other CODES that resolve to this language, derived from
|
|
129
|
+
// SIL's own ISO 639-1 column: a language holding Part1 'fr' is what 'fr'
|
|
130
|
+
// means. This was briefly called `aliases`, which invited confusion with
|
|
131
|
+
// `alternateNames` (which are NAMES: "Northern Tosk Albanian", not "sq").
|
|
132
|
+
// The two were once merged by mistake and every two-letter lookup broke.
|
|
133
|
+
const codeAliases = card.codeAliases ?? card.aliases;
|
|
134
|
+
if (Array.isArray(codeAliases)) {
|
|
135
|
+
for (const alias of codeAliases) {
|
|
136
|
+
_aliases.set(alias, card.code);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Load all language card JSON files from the cards directory (recursively).
|
|
143
|
+
* Called once at module initialization. Builds both the primary
|
|
144
|
+
* card registry and the alias lookup table.
|
|
145
|
+
*
|
|
146
|
+
* When the directory is absent (published npm package), falls back to
|
|
147
|
+
* the bundled core set + manifest (shared/cards-fallback.json).
|
|
148
|
+
*/
|
|
149
|
+
function _loadCards() {
|
|
150
|
+
if (!fs.existsSync(CARDS_DIR)) {
|
|
151
|
+
_loadFallbackBundle();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
_mode = 'repo';
|
|
155
|
+
|
|
156
|
+
// The five x-* product locales are configuration, not languages — no source
|
|
157
|
+
// will ever publish x-pirate, and under the fetch-from-source law they have
|
|
158
|
+
// no place in the atlas. They are still product features users invoke, so
|
|
159
|
+
// when the cards directory is the projected atlas (which contains only
|
|
160
|
+
// languages), they are served from the config kernel. A live-corpus dir
|
|
161
|
+
// still carries them as files and wins by being loaded later.
|
|
162
|
+
_loadPromptConfig();
|
|
163
|
+
for (const [code, card] of Object.entries(_cardConfig.conlangs)) {
|
|
164
|
+
if (!_cards.has(code)) _cards.set(code, card);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function scan(dir) {
|
|
168
|
+
let entries;
|
|
169
|
+
try {
|
|
170
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
171
|
+
} catch {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
const resPath = path.join(dir, entry.name);
|
|
177
|
+
if (entry.isDirectory()) {
|
|
178
|
+
scan(resPath);
|
|
179
|
+
} else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
180
|
+
try {
|
|
181
|
+
const raw = fs.readFileSync(resPath, 'utf-8');
|
|
182
|
+
// Normalised at the ONE load site: atlas-shaped cards carry honest
|
|
183
|
+
// names (endonym, textDirection, scripts[]) and attribution
|
|
184
|
+
// envelopes; the 24 modules importing this registry read the legacy
|
|
185
|
+
// vocabulary. The reader adapts so neither the atlas has to lie nor
|
|
186
|
+
// 24 modules have to change mid-cutover. Old-shape cards pass
|
|
187
|
+
// through untouched.
|
|
188
|
+
const card = normalizeCard(JSON.parse(raw));
|
|
189
|
+
|
|
190
|
+
// Data files (e.g., language-tree.json) have a _meta key and no
|
|
191
|
+
// 'code' field — they are reference data, not language cards.
|
|
192
|
+
// Skip them silently instead of logging a noisy warning.
|
|
193
|
+
if (card._meta) continue;
|
|
194
|
+
|
|
195
|
+
if (!card.code) {
|
|
196
|
+
console.error(`[WARN] Language card ${entry.name} missing 'code' field — skipping.`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (
|
|
201
|
+
resPath.includes(path.sep + 'genera' + path.sep) ||
|
|
202
|
+
card.code.startsWith('family-') ||
|
|
203
|
+
card.code.startsWith('genus-') ||
|
|
204
|
+
card.code.startsWith('macrolanguage-')
|
|
205
|
+
) {
|
|
206
|
+
_parentCards.set(card.code, card);
|
|
207
|
+
} else {
|
|
208
|
+
_registerCard(card);
|
|
209
|
+
}
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.error(`[WARN] Failed to load language card ${entry.name}: ${err.message}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
scan(CARDS_DIR);
|
|
218
|
+
_applyCodeRouting();
|
|
219
|
+
_loadPromptConfig();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Translation-prompt CONFIGURATION, loaded once.
|
|
224
|
+
*
|
|
225
|
+
* A card asserts what SOURCES say about a language. What register presets to
|
|
226
|
+
* offer, how to phrase gender guidance, what we call a politeness system —
|
|
227
|
+
* none of that is asserted by anyone, so it lives in shared/catalogue/ and is
|
|
228
|
+
* composed onto the card at read time.
|
|
229
|
+
*
|
|
230
|
+
* The previous corpus kept all of it ON the cards: 1,775 register blocks
|
|
231
|
+
* carrying 110 distinct prompt objects, 2,404 gender blocks of which only 103
|
|
232
|
+
* held anything a runtime reads. Worse, the presets were stamped
|
|
233
|
+
* `source: "grambank-GB415"` — citing Grambank for text we wrote, which is why
|
|
234
|
+
* config and fact became impossible to tell apart.
|
|
235
|
+
*
|
|
236
|
+
* Storage keeps them separate. The runtime VIEW puts them back together, so
|
|
237
|
+
* consumers still see card.registers and card.formality.
|
|
238
|
+
*/
|
|
239
|
+
const _presets = { shared: {}, byLanguage: {}, assignment: {} };
|
|
240
|
+
/** Languages DeepL documents formality for — cite-only, from method-coverage.json. */
|
|
241
|
+
const _deeplFormality = new Set();
|
|
242
|
+
/** Per-metric pivot sets + QE models from metric-coverage.json (the register). */
|
|
243
|
+
const _metricCoverage = { pivots: {}, qe: {} };
|
|
244
|
+
/** Gate 3: authored product config, attached to the view, never cited as fact. */
|
|
245
|
+
const _cardConfig = { scriptConverter: {}, conlangs: {}, aliasRouting: {}, evalConfig: {} };
|
|
246
|
+
/** Indexed once at load: entry lists keyed by what each applies to. */
|
|
247
|
+
let _genderGuidance = { family: new Map(), language: new Map() };
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Where the prompt config lives, in both layouts.
|
|
251
|
+
*
|
|
252
|
+
* In the repo it is `shared/catalogue/` at the monorepo root. In a PUBLISHED
|
|
253
|
+
* package there is no monorepo root — npm ships `cli/shared/`, so the same
|
|
254
|
+
* files arrive one level up from `lib/`. Reading only the repo path meant a
|
|
255
|
+
* published install found nothing, and the catch below turned that into
|
|
256
|
+
* silence: every language lost its formality system and register prompts, and
|
|
257
|
+
* `champollion` translated German with no notion of Sie versus du.
|
|
258
|
+
*
|
|
259
|
+
* Both are tried, packaged first, because that is the layout a user has.
|
|
260
|
+
*/
|
|
261
|
+
const _CONFIG_DIRS = [
|
|
262
|
+
path.join(import.meta.dirname, '..', 'shared', 'catalogue'),
|
|
263
|
+
path.join(import.meta.dirname, '..', '..', 'shared', 'catalogue'),
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
/** Read a config file from whichever layout has it. Returns null if neither. */
|
|
267
|
+
function _readConfig(name) {
|
|
268
|
+
for (const dir of _CONFIG_DIRS) {
|
|
269
|
+
try {
|
|
270
|
+
return JSON.parse(fs.readFileSync(path.join(dir, name), 'utf-8'));
|
|
271
|
+
} catch { /* try the next layout */ }
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Config files that were looked for and not found — reported, never silent. */
|
|
277
|
+
export const missingPromptConfig = [];
|
|
278
|
+
|
|
279
|
+
function _loadPromptConfig() {
|
|
280
|
+
{
|
|
281
|
+
const p = _readConfig('register-presets.json');
|
|
282
|
+
if (p) {
|
|
283
|
+
Object.assign(_presets, {
|
|
284
|
+
shared: p.shared ?? {}, byLanguage: p.byLanguage ?? {}, assignment: p.assignment ?? {},
|
|
285
|
+
});
|
|
286
|
+
} else {
|
|
287
|
+
// Not fatal — a partial checkout is legitimate and cards still work —
|
|
288
|
+
// but recorded, so `champollion doctor` and the tests can see that this
|
|
289
|
+
// install has no register prompts rather than inferring that no
|
|
290
|
+
// language has a politeness system.
|
|
291
|
+
missingPromptConfig.push('register-presets.json');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
{
|
|
295
|
+
const cc = _readConfig('card-config.json');
|
|
296
|
+
if (cc) {
|
|
297
|
+
Object.assign(_cardConfig.scriptConverter, cc.scriptConverter ?? {});
|
|
298
|
+
Object.assign(_cardConfig.conlangs, cc.conlangs ?? {});
|
|
299
|
+
for (const [k, v] of Object.entries(cc.aliasRouting ?? {})) {
|
|
300
|
+
if (!k.startsWith('_')) _cardConfig.aliasRouting[k] = v;
|
|
301
|
+
}
|
|
302
|
+
// Per-language eval wiring (evalStandard/evalMetrics/evalPack/
|
|
303
|
+
// evalDatasets) — CONFIG, ours, attached at read time exactly like the
|
|
304
|
+
// Python twin's _eval_config_for. The atlas deliberately does not
|
|
305
|
+
// project these (they are wiring, not facts about a language), and the
|
|
306
|
+
// JS seam never picked them up, so `champollion card crk` lost its eval
|
|
307
|
+
// section and build-metric-pops' seal check stopped loud at the gap.
|
|
308
|
+
for (const [k, v] of Object.entries(cc.evalConfig ?? {})) {
|
|
309
|
+
if (!k.startsWith('_')) _cardConfig.evalConfig[k] = v;
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
missingPromptConfig.push('card-config.json');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
{
|
|
316
|
+
// DeepL's formality support is a METHOD capability — whether the service
|
|
317
|
+
// accepts a formality argument for a language — and is deliberately read
|
|
318
|
+
// from method coverage rather than from anything on the language card.
|
|
319
|
+
const mc = _readConfig('method-coverage.json');
|
|
320
|
+
if (mc) {
|
|
321
|
+
const dl = (mc.methods ?? []).find((m) => m.key === 'deepl');
|
|
322
|
+
for (const code of dl?.formalityIso6393 ?? []) _deeplFormality.add(code);
|
|
323
|
+
} else {
|
|
324
|
+
missingPromptConfig.push('method-coverage.json');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
{
|
|
328
|
+
// metric-coverage.json is the register behind the metricModelSupport
|
|
329
|
+
// flattening below: which listed codes are a publisher's PIVOTS (in the
|
|
330
|
+
// list as pass-through languages, never targets) and which metric has a
|
|
331
|
+
// QE checkpoint. Python twin: _metric_pivots() / _metric_qe_models().
|
|
332
|
+
const mcov = _readConfig('metric-coverage.json');
|
|
333
|
+
if (mcov) {
|
|
334
|
+
for (const m of mcov.models ?? []) {
|
|
335
|
+
_metricCoverage.pivots[m.key] = new Set(m.pivots ?? []);
|
|
336
|
+
if (m.qeModel) _metricCoverage.qe[m.key] = m.qeModel;
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
missingPromptConfig.push('metric-coverage.json');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
{
|
|
343
|
+
const g = _readConfig('gender-guidance.json');
|
|
344
|
+
if (!g) {
|
|
345
|
+
missingPromptConfig.push('gender-guidance.json');
|
|
346
|
+
_genderGuidance = { family: new Map(), language: new Map() };
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const family = new Map();
|
|
350
|
+
const language = new Map();
|
|
351
|
+
for (const e of g.entries ?? []) {
|
|
352
|
+
// Genus entries are carried but not applied: a genus sits below a family
|
|
353
|
+
// and needs a subgroup match against the ancestry chain, not a name
|
|
354
|
+
// lookup. `applied: false` keeps the gap visible instead of silent.
|
|
355
|
+
if (e.scope === 'genus' || e.applied === false) continue;
|
|
356
|
+
const target = e.scope === 'language' ? language : family;
|
|
357
|
+
for (const key of e.appliesTo ?? []) target.set(key, e.guidance);
|
|
358
|
+
}
|
|
359
|
+
_genderGuidance = { family, language };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Gender guidance for a language: its own if someone wrote one, otherwise its
|
|
365
|
+
* family's.
|
|
366
|
+
*
|
|
367
|
+
* "Bantu languages use noun class systems" is a statement about the FAMILY. It
|
|
368
|
+
* is true and it is useful prompt text, and it is not a fact about any one
|
|
369
|
+
* language — so it is stored once per family and resolved here, rather than
|
|
370
|
+
* copied onto 3,469 cards the way hub inheritance used to do it.
|
|
371
|
+
*
|
|
372
|
+
* The family is ATTRIBUTED: Glottolog and WALS are separate taxonomies that
|
|
373
|
+
* disagree on 905 cards (Atlantic-Congo vs Niger-Congo, and so on). Both names
|
|
374
|
+
* are tried, because guidance written against one taxonomy's name should not
|
|
375
|
+
* vanish because the other is listed first.
|
|
376
|
+
*/
|
|
377
|
+
function _genderGuidanceFor(card) {
|
|
378
|
+
const own = _genderGuidance.language.get(card.code);
|
|
379
|
+
if (own) return own;
|
|
380
|
+
const fam = card.classification?.family;
|
|
381
|
+
const names = fam?.values ? fam.values.map((v) => v.value) : (fam ? [fam] : []);
|
|
382
|
+
for (const n of names) {
|
|
383
|
+
const hit = _genderGuidance.family.get(n);
|
|
384
|
+
if (hit) return hit;
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** The preset for a language, from either the bespoke set or a shared one. */
|
|
390
|
+
function _presetFor(code) {
|
|
391
|
+
return _presets.byLanguage[code]
|
|
392
|
+
?? _presets.shared[_presets.assignment[code]]
|
|
393
|
+
?? null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Compose the runtime view: facts from the card, prompts from the config.
|
|
398
|
+
*
|
|
399
|
+
* A card that already carries `registers` keeps them — during migration the
|
|
400
|
+
* live corpus still does, and a card's own values must not be silently
|
|
401
|
+
* replaced by config. Once cutover lands, only the config path is live.
|
|
402
|
+
*/
|
|
403
|
+
function _withPromptConfig(card) {
|
|
404
|
+
if (!card) return card;
|
|
405
|
+
const preset = _presetFor(card.code);
|
|
406
|
+
const guidance = _genderGuidanceFor(card);
|
|
407
|
+
|
|
408
|
+
const view = { ...card };
|
|
409
|
+
// ── Migration seam, removed at cutover ────────────────────────────────────
|
|
410
|
+
// The live corpus still carries plural categories at rules.plurals.categories
|
|
411
|
+
// and text direction as `dir`. The projected corpus carries them as the
|
|
412
|
+
// sourced parameters pluralCategories and textDirection. Consumers read the
|
|
413
|
+
// NEW names; this maps a legacy card forward so the two corpora can be tested
|
|
414
|
+
// against the same code without either being broken.
|
|
415
|
+
//
|
|
416
|
+
// ONE mapping, in ONE place, with an end date — not a compatibility layer
|
|
417
|
+
// threaded through the consumers.
|
|
418
|
+
if (!view.pluralCategories && Array.isArray(view.rules?.plurals?.categories)) {
|
|
419
|
+
view.pluralCategories = view.rules.plurals.categories;
|
|
420
|
+
}
|
|
421
|
+
if (!view.textDirection && view.dir === 'rtl') view.textDirection = 'right-to-left';
|
|
422
|
+
|
|
423
|
+
// Eval wiring from card-config.json (Python twin: _eval_config_for +
|
|
424
|
+
// setdefault). A card's own values are never replaced — config fills only
|
|
425
|
+
// what the card does not carry.
|
|
426
|
+
const evalWiring = _cardConfig.evalConfig[view.code];
|
|
427
|
+
if (evalWiring) {
|
|
428
|
+
for (const [k, v] of Object.entries(evalWiring)) {
|
|
429
|
+
if (view[k] === undefined) view[k] = v;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// The atlas projects methodSupport as EVIDENCE — {total, byTier, named[]} —
|
|
434
|
+
// because English attracts 5,420 model-card claims and a flat map of those
|
|
435
|
+
// is a log, not an index. The runtime's consumers ask a smaller question:
|
|
436
|
+
// "does this SERVICE cover this language, and does DeepL do formality here?"
|
|
437
|
+
// Service entries are named in full, above the projection cap, precisely so
|
|
438
|
+
// that absence answers that question honestly. This synthesizes the legacy
|
|
439
|
+
// map from them; the evidence stays on the card as methodSupportEvidence.
|
|
440
|
+
if (view.methodSupport && Array.isArray(view.methodSupport.named)) {
|
|
441
|
+
const LEGACY_KEYS = {
|
|
442
|
+
'google-translate': 'googleTranslate',
|
|
443
|
+
deepl: 'deepl',
|
|
444
|
+
'microsoft-translator': 'microsoftTranslator',
|
|
445
|
+
libretranslate: 'libreTranslate',
|
|
446
|
+
apertium: 'apertium',
|
|
447
|
+
};
|
|
448
|
+
const flat = {};
|
|
449
|
+
for (const key of Object.values(LEGACY_KEYS)) flat[key] = { supported: false };
|
|
450
|
+
for (const n of view.methodSupport.named) {
|
|
451
|
+
const legacy = LEGACY_KEYS[n.variant];
|
|
452
|
+
if (legacy && n.value === 'service') flat[legacy] = { supported: true };
|
|
453
|
+
}
|
|
454
|
+
flat.deepl.formality = flat.deepl.supported && _deeplFormality.has(view.code);
|
|
455
|
+
// The LLM lane is not list-based coverage: the method registry declares
|
|
456
|
+
// the adapters, and their coverage is open-ended by construction — an LLM
|
|
457
|
+
// will ATTEMPT any language, which is a fact about the method's shape,
|
|
458
|
+
// not a per-language claim anyone fetched. Quality is the leaderboard's
|
|
459
|
+
// question, never this flag's.
|
|
460
|
+
flat.llm = { supported: true };
|
|
461
|
+
view.methodSupportEvidence = view.methodSupport;
|
|
462
|
+
view.methodSupport = flat;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// metricModelSupport: same adaptation, twin of the Python adapter's block.
|
|
466
|
+
// The atlas records "which metric model lists this language" as attributed
|
|
467
|
+
// values with a metric variant; the runtime reads a flat map. xlmr membership
|
|
468
|
+
// IS the high tier — the paper publishes the list, not per-language scores
|
|
469
|
+
// (tierMeaning on the register entry). A publisher's PIVOTS (eng/fra for
|
|
470
|
+
// AfriCOMET) are in the list as the languages pairs run through, not as
|
|
471
|
+
// targets — the register marks them; treating a pivot as a target would
|
|
472
|
+
// recommend africomet for French.
|
|
473
|
+
const mms = view.metricModelSupport;
|
|
474
|
+
if (mms && typeof mms === 'object' && Array.isArray(mms.values)) {
|
|
475
|
+
const flatM = {};
|
|
476
|
+
const code3 = view.iso639_3 ?? view.code;
|
|
477
|
+
for (const v of mms.values) {
|
|
478
|
+
const variant = v?.variant;
|
|
479
|
+
if (!variant) continue;
|
|
480
|
+
if (variant === 'xlmr') {
|
|
481
|
+
flatM[variant] = { tier: 'high', model: v.value };
|
|
482
|
+
} else {
|
|
483
|
+
const pivots = _metricCoverage.pivots[variant] ?? new Set();
|
|
484
|
+
flatM[variant] = { supported: !pivots.has(code3), model: v.value };
|
|
485
|
+
if (pivots.has(code3)) flatM[variant].pivot = true;
|
|
486
|
+
if (variant === 'africomet' && _metricCoverage.qe[variant]) {
|
|
487
|
+
flatM.qe = { ...flatM[variant], model: _metricCoverage.qe[variant] };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
view.metricModelSupport = flatM;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (preset && !view.registers) {
|
|
495
|
+
view.registers = preset.registers;
|
|
496
|
+
view.formality = {
|
|
497
|
+
...(view.formality ?? {}),
|
|
498
|
+
...(preset.system ? { system: preset.system } : {}),
|
|
499
|
+
...(preset.default ? { default: preset.default } : {}),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
if (guidance) view.gender = { ...(view.gender ?? {}), inclusiveGuidance: guidance };
|
|
503
|
+
// Gate 3: the converter key is OUR choice of which script conversion the CLI
|
|
504
|
+
// performs for a locale — product config, not a fact about the language, so
|
|
505
|
+
// it rides the view from the config kernel and is never cited.
|
|
506
|
+
if (!view.scriptConverter && _cardConfig.scriptConverter[view.code]) {
|
|
507
|
+
view.scriptConverter = _cardConfig.scriptConverter[view.code];
|
|
508
|
+
}
|
|
509
|
+
return view;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Route codes that do not identify ONE language.
|
|
514
|
+
*
|
|
515
|
+
* `fr` unambiguously means French, and that alias comes off the card itself.
|
|
516
|
+
* `zh` does not mean one language — it names the Chinese macrolanguage — and
|
|
517
|
+
* deciding it should reach Mandarin is a CHOICE. Those choices live in
|
|
518
|
+
* shared/catalogue/code-routing.json with their rationale, not on a card,
|
|
519
|
+
* because "when someone types zh, give them Mandarin" is not something
|
|
520
|
+
* Glottolog or SIL says and putting it on a card would dress our decision up
|
|
521
|
+
* as an upstream fact.
|
|
522
|
+
*
|
|
523
|
+
* Card-derived aliases win: a real language holding an ISO 639-1 code is never
|
|
524
|
+
* overridden by a routing preference.
|
|
525
|
+
*/
|
|
526
|
+
function _applyCodeRouting() {
|
|
527
|
+
const file = path.join(
|
|
528
|
+
import.meta.dirname, '..', '..', 'shared', 'catalogue', 'code-routing.json',
|
|
529
|
+
);
|
|
530
|
+
if (!fs.existsSync(file)) return;
|
|
531
|
+
let routes;
|
|
532
|
+
try {
|
|
533
|
+
({ routes } = JSON.parse(fs.readFileSync(file, 'utf-8')));
|
|
534
|
+
} catch (err) {
|
|
535
|
+
console.error(`[WARN] code-routing.json is unreadable: ${err.message}`);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
for (const [from, route] of Object.entries(routes ?? {})) {
|
|
539
|
+
if (_aliases.has(from) || _cards.has(from)) continue;
|
|
540
|
+
if (_cards.has(route?.to)) _aliases.set(from, route.to);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Config ROUTING decisions override derived aliases. ISO maps `no` to the
|
|
544
|
+
// macrolanguage nor; the product routes it to nob, a recorded decision in
|
|
545
|
+
// card-config.json with its reasoning — so it must win over the alias the
|
|
546
|
+
// registries would derive, or the decision silently loses to the derivation.
|
|
547
|
+
for (const [from, to] of Object.entries(_cardConfig.aliasRouting)) {
|
|
548
|
+
if (_cards.has(to)) _aliases.set(from, to);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Packaged mode: load the bundled fallback (core cards + parents +
|
|
554
|
+
* full manifest) produced by scripts/build-cards-fallback.mjs.
|
|
555
|
+
*/
|
|
556
|
+
function _loadFallbackBundle() {
|
|
557
|
+
// PACKAGED MODE NEEDS THE PROMPT CONFIG TOO — and used not to load it.
|
|
558
|
+
//
|
|
559
|
+
// _loadCards() called _loadPromptConfig() only on the repo branch, so the
|
|
560
|
+
// one mode every published user actually runs in loaded no register presets
|
|
561
|
+
// at all. Formality systems, register prompts, script converters and the
|
|
562
|
+
// conlang locales were all absent, silently, for everyone who installed from
|
|
563
|
+
// npm — while every test passed, because tests run in repo mode.
|
|
564
|
+
//
|
|
565
|
+
// Loaded first, so the conlang locales below can come from the config kernel
|
|
566
|
+
// exactly as they do in repo mode.
|
|
567
|
+
_loadPromptConfig();
|
|
568
|
+
for (const [code, card] of Object.entries(_cardConfig.conlangs)) {
|
|
569
|
+
if (!_cards.has(code)) _cards.set(code, card);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
let bundle;
|
|
573
|
+
try {
|
|
574
|
+
bundle = JSON.parse(fs.readFileSync(FALLBACK_FILE, 'utf-8'));
|
|
575
|
+
} catch {
|
|
576
|
+
return; // _mode stays 'none' — registry is empty, same as v6 without cards
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
for (const card of Object.values(bundle.parents || {})) {
|
|
580
|
+
if (card?.code) _parentCards.set(card.code, card);
|
|
581
|
+
}
|
|
582
|
+
for (const card of Object.values(bundle.cards || {})) {
|
|
583
|
+
if (card?.code) _registerCard(card);
|
|
584
|
+
}
|
|
585
|
+
for (const [code, entry] of Object.entries(bundle.manifest || {})) {
|
|
586
|
+
_manifest.set(code, entry);
|
|
587
|
+
// Manifest aliases cover the whole catalogue, so 'fr'-style lookups
|
|
588
|
+
// resolve even for languages that aren't materialized locally yet.
|
|
589
|
+
if (Array.isArray(entry.a)) {
|
|
590
|
+
for (const alias of entry.a) {
|
|
591
|
+
if (!_aliases.has(alias)) _aliases.set(alias, code);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
_mode = 'packaged';
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Load cards at module initialization
|
|
599
|
+
_loadCards();
|
|
600
|
+
|
|
601
|
+
// -----------------------------------------------------------------
|
|
602
|
+
// Dynamic tier (packaged mode) — per-user cache + remote fetch
|
|
603
|
+
// -----------------------------------------------------------------
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Try to materialize a card that isn't bundled: first from the
|
|
607
|
+
* per-user cache (~/.champollion/cards/), then via a synchronous
|
|
608
|
+
* network fetch. Registers the card on success.
|
|
609
|
+
*
|
|
610
|
+
* @param {string} code - already-resolved primary code
|
|
611
|
+
* @returns {boolean} true when the card is now in _cards
|
|
612
|
+
*/
|
|
613
|
+
function _loadDynamicCard(code) {
|
|
614
|
+
if (!isValidCardCode(code)) return false;
|
|
615
|
+
|
|
616
|
+
const cached = readCachedCard(code);
|
|
617
|
+
if (cached) {
|
|
618
|
+
_registerCard(cached.card);
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const fetched = _tryFetchRemoteSync(code);
|
|
623
|
+
if (fetched) {
|
|
624
|
+
_registerCard(fetched);
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Synchronous remote fetch via a short-lived child process (Node has
|
|
632
|
+
* no synchronous HTTP, and getLanguageCard() is part of the stable
|
|
633
|
+
* synchronous API). Guarded so it runs at most once per code per
|
|
634
|
+
* process, never when offline, never inside the failure backoff
|
|
635
|
+
* window, and never for codes prod has confirmed it doesn't have.
|
|
636
|
+
*
|
|
637
|
+
* @returns {object|null} the fetched card, or null
|
|
638
|
+
*/
|
|
639
|
+
function _tryFetchRemoteSync(code) {
|
|
640
|
+
if (OFFLINE) return null;
|
|
641
|
+
if (_fetchAttempted.has(code)) return null;
|
|
642
|
+
_fetchAttempted.add(code);
|
|
643
|
+
|
|
644
|
+
const state = readState();
|
|
645
|
+
if (inBackoff(state) || isTombstoned(code, state)) return null;
|
|
646
|
+
|
|
647
|
+
const aliases = _manifest.get(code)?.a;
|
|
648
|
+
let stdout;
|
|
649
|
+
try {
|
|
650
|
+
stdout = execFileSync(
|
|
651
|
+
process.execPath,
|
|
652
|
+
[FETCH_CHILD, code, aliases ? JSON.stringify(aliases) : ''],
|
|
653
|
+
{
|
|
654
|
+
encoding: 'utf-8',
|
|
655
|
+
timeout: 20000,
|
|
656
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
657
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
658
|
+
},
|
|
659
|
+
);
|
|
660
|
+
} catch {
|
|
661
|
+
noteNetworkFailure();
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
let result;
|
|
666
|
+
try {
|
|
667
|
+
result = JSON.parse(stdout);
|
|
668
|
+
} catch {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (!result.ok) {
|
|
673
|
+
noteNetworkFailure();
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
if (result.missing) {
|
|
677
|
+
tombstone(code);
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
writeCachedCard(code, result.card, result.updatedAt);
|
|
682
|
+
return result.card;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Prefetch cards for a set of locale codes into the per-user cache
|
|
687
|
+
* (packaged mode; repo mode is a no-op since everything is local).
|
|
688
|
+
*
|
|
689
|
+
* Async batch alternative to the per-miss synchronous fetch — call it
|
|
690
|
+
* up front (e.g. before a sync run over many target locales) to warm
|
|
691
|
+
* the cache in one pass. Network failures are reported, not thrown.
|
|
692
|
+
*
|
|
693
|
+
* @param {string[]} codes - locale codes (aliases are resolved)
|
|
694
|
+
* @returns {Promise<{fetched: string[], missing: string[], failed: string[], skipped: string[]}>}
|
|
695
|
+
*/
|
|
696
|
+
async function prefetchLanguageCards(codes) {
|
|
697
|
+
const result = { fetched: [], missing: [], failed: [], skipped: [] };
|
|
698
|
+
if (_mode !== 'packaged' || OFFLINE || !Array.isArray(codes)) {
|
|
699
|
+
result.skipped = Array.isArray(codes) ? [...codes] : [];
|
|
700
|
+
return result;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
for (const input of codes) {
|
|
704
|
+
const code = resolveCode(input);
|
|
705
|
+
if (!isValidCardCode(code) || _cards.has(code) || readCachedCard(code)) {
|
|
706
|
+
result.skipped.push(input);
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
try {
|
|
710
|
+
const remote = await fetchRemoteCard(code, { aliases: _manifest.get(code)?.a });
|
|
711
|
+
if (remote.missing) {
|
|
712
|
+
tombstone(code);
|
|
713
|
+
result.missing.push(code);
|
|
714
|
+
} else {
|
|
715
|
+
writeCachedCard(code, remote.card, remote.updatedAt);
|
|
716
|
+
_registerCard(remote.card);
|
|
717
|
+
result.fetched.push(code);
|
|
718
|
+
}
|
|
719
|
+
} catch {
|
|
720
|
+
noteNetworkFailure();
|
|
721
|
+
result.failed.push(code);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return result;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Where cards are coming from in this process — for doctor output,
|
|
729
|
+
* debugging, and tests.
|
|
730
|
+
*/
|
|
731
|
+
function getCardSourceInfo() {
|
|
732
|
+
return {
|
|
733
|
+
mode: _mode,
|
|
734
|
+
loadedCards: _cards.size,
|
|
735
|
+
parentCards: _parentCards.size,
|
|
736
|
+
manifestEntries: _manifest.size,
|
|
737
|
+
cachedCards: _mode === 'packaged' ? listCachedCodes().length : 0,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// -----------------------------------------------------------------
|
|
742
|
+
// Public API — accessor functions for language cards and registers
|
|
743
|
+
// -----------------------------------------------------------------
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Resolve a locale code to its primary ISO 639-3 code, following aliases.
|
|
747
|
+
*
|
|
748
|
+
* Handles:
|
|
749
|
+
* - Direct match: 'fra' → 'fra'
|
|
750
|
+
* - Alias: 'fr' → 'fra', 'no' → 'nob', 'iw' → 'heb'
|
|
751
|
+
* - Base locale fallback: 'deu-AT' → 'deu' (base)
|
|
752
|
+
*
|
|
753
|
+
* @param {string} code - Locale code to resolve
|
|
754
|
+
* @returns {string} Resolved primary code (ISO 639-3)
|
|
755
|
+
*/
|
|
756
|
+
function resolveCode(code) {
|
|
757
|
+
// Direct match
|
|
758
|
+
if (_cards.has(code)) return code;
|
|
759
|
+
|
|
760
|
+
// Alias match
|
|
761
|
+
if (_aliases.has(code)) return _aliases.get(code);
|
|
762
|
+
|
|
763
|
+
// Manifest match (packaged mode) — the code is a known primary code
|
|
764
|
+
// even though its card isn't materialized locally yet
|
|
765
|
+
if (_manifest.has(code)) return code;
|
|
766
|
+
|
|
767
|
+
// Base locale fallback — try stripping region (e.g., 'de-AT' → 'de')
|
|
768
|
+
const baseParts = code.split('-');
|
|
769
|
+
if (baseParts.length > 1) {
|
|
770
|
+
const baseCode = baseParts[0];
|
|
771
|
+
if (_cards.has(baseCode)) return baseCode;
|
|
772
|
+
if (_aliases.has(baseCode)) return _aliases.get(baseCode);
|
|
773
|
+
if (_manifest.has(baseCode)) return baseCode;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// No match found — return original code (will get null from getLanguageCard)
|
|
777
|
+
return code;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Deep merge two card objects (child overrides parent).
|
|
782
|
+
*
|
|
783
|
+
* Semantics:
|
|
784
|
+
* - null in child → inherit from parent (don't override)
|
|
785
|
+
* - non-null in child → override parent
|
|
786
|
+
* - objects merge recursively (child fields override, parent fields kept)
|
|
787
|
+
* - arrays in child replace parent arrays entirely
|
|
788
|
+
* - identity fields (code, extends, _migration) are never inherited from parent
|
|
789
|
+
* - hub-only fields (members, supportTier, taxonomyNotes) are never
|
|
790
|
+
* inherited from parent: genera/macrolanguage hub cards double as
|
|
791
|
+
* `extends` templates for their member cards, but those fields
|
|
792
|
+
* describe the hub itself (see derive-taxonomy-fields.mjs header +
|
|
793
|
+
* docs/LANGUAGE_TAXONOMY.md) — e.g. arz must not surface ara's
|
|
794
|
+
* members[] when runtime-resolved
|
|
795
|
+
*/
|
|
796
|
+
const _IDENTITY_FIELDS = new Set(['code', 'extends', '_migration', 'aliases', 'iso639_1', 'iso639_3']);
|
|
797
|
+
const _NON_INHERITED_FIELDS = new Set(['members', 'supportTier', 'taxonomyNotes']);
|
|
798
|
+
|
|
799
|
+
function _deepMerge(parent, child) {
|
|
800
|
+
if (!parent) return child || {};
|
|
801
|
+
if (!child) return parent || {};
|
|
802
|
+
|
|
803
|
+
const result = { ...parent };
|
|
804
|
+
// Top-level card merges only (cards carry `code`; nested objects don't):
|
|
805
|
+
// drop hub-only fields coming from the parent. The child's own values,
|
|
806
|
+
// if any, are re-applied by the merge loop below.
|
|
807
|
+
if (Object.prototype.hasOwnProperty.call(parent, 'code')) {
|
|
808
|
+
for (const key of _NON_INHERITED_FIELDS) delete result[key];
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
for (const [key, value] of Object.entries(child)) {
|
|
812
|
+
// Identity fields: always use child's value (even if null)
|
|
813
|
+
if (_IDENTITY_FIELDS.has(key)) {
|
|
814
|
+
result[key] = value;
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Null in child means "I don't define this" → inherit from parent
|
|
819
|
+
if (value === null || value === undefined) {
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (
|
|
824
|
+
typeof value === 'object' &&
|
|
825
|
+
!Array.isArray(value) &&
|
|
826
|
+
parent[key] &&
|
|
827
|
+
typeof parent[key] === 'object' &&
|
|
828
|
+
!Array.isArray(parent[key])
|
|
829
|
+
) {
|
|
830
|
+
result[key] = _deepMerge(parent[key], value);
|
|
831
|
+
} else {
|
|
832
|
+
result[key] = value;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return result;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* In-memory cache of fully resolved/merged language cards.
|
|
840
|
+
* Keyed by primary locale code.
|
|
841
|
+
* @type {Map<string, object>}
|
|
842
|
+
*/
|
|
843
|
+
const _resolvedCards = new Map();
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Get the full language card for a locale code.
|
|
847
|
+
*
|
|
848
|
+
* Follows aliases and falls back to base locale. Returns null if
|
|
849
|
+
* no card exists for this language.
|
|
850
|
+
*
|
|
851
|
+
* Resolves inheritance chains recursively if the 'extends' property is set.
|
|
852
|
+
*
|
|
853
|
+
* @param {string} code - Locale code (e.g., 'fr', 'ko', 'no', 'fr-CA')
|
|
854
|
+
* @returns {object|null} Complete language card, or null
|
|
855
|
+
*/
|
|
856
|
+
function getLanguageCard(code) {
|
|
857
|
+
// First check parents (no alias/base resolution needed for parent cards)
|
|
858
|
+
if (_parentCards.has(code)) {
|
|
859
|
+
if (_resolvedCards.has(code)) {
|
|
860
|
+
return _resolvedCards.get(code);
|
|
861
|
+
}
|
|
862
|
+
const rawCard = _parentCards.get(code);
|
|
863
|
+
let resolvedCard = rawCard;
|
|
864
|
+
if (rawCard.extends) {
|
|
865
|
+
const parentCard = getLanguageCard(rawCard.extends);
|
|
866
|
+
if (parentCard) {
|
|
867
|
+
resolvedCard = _deepMerge(parentCard, rawCard);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const composed = _withPromptConfig(resolvedCard);
|
|
871
|
+
_resolvedCards.set(code, composed);
|
|
872
|
+
return composed;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const resolved = resolveCode(code);
|
|
876
|
+
if (!_cards.has(resolved)) {
|
|
877
|
+
// Packaged mode: try the per-user cache, then a synchronous remote
|
|
878
|
+
// fetch. Both register the card into _cards on success.
|
|
879
|
+
if (_mode !== 'packaged' || !_loadDynamicCard(resolved)) return null;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (_resolvedCards.has(resolved)) {
|
|
883
|
+
return _resolvedCards.get(resolved);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const rawCard = _cards.get(resolved);
|
|
887
|
+
let resolvedCard = rawCard;
|
|
888
|
+
|
|
889
|
+
if (rawCard.extends) {
|
|
890
|
+
const parentCard = getLanguageCard(rawCard.extends);
|
|
891
|
+
if (parentCard) {
|
|
892
|
+
resolvedCard = _deepMerge(parentCard, rawCard);
|
|
893
|
+
} else {
|
|
894
|
+
console.error(`[WARN] Language card '${resolved}' extends unknown card '${rawCard.extends}'.`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Facts from the card, prompts from shared/catalogue/. Composed once and
|
|
899
|
+
// cached, so consumers see one object and never learn that two files exist.
|
|
900
|
+
const composed = _withPromptConfig(resolvedCard);
|
|
901
|
+
_resolvedCards.set(resolved, composed);
|
|
902
|
+
return composed;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Get the active register prompt text for a locale.
|
|
907
|
+
*
|
|
908
|
+
* Resolution order:
|
|
909
|
+
* 1. If presetOrCustom matches a preset key in the card → use that preset's prompt
|
|
910
|
+
* 2. If presetOrCustom is a non-empty string that doesn't match a preset → treat as custom text
|
|
911
|
+
* 3. If presetOrCustom is null/undefined → use the card's default preset
|
|
912
|
+
* 4. If no card exists → return generic fallback
|
|
913
|
+
*
|
|
914
|
+
* @param {string} code - Locale code
|
|
915
|
+
* @param {string|null} [presetOrCustom] - Preset name (e.g., 'formal-vous') or custom register text
|
|
916
|
+
* @returns {string} Register prompt text
|
|
917
|
+
*/
|
|
918
|
+
|
|
919
|
+
/** Fallback register text when no card or preset provides one. */
|
|
920
|
+
const DEFAULT_REGISTER_FALLBACK = 'Professional register.';
|
|
921
|
+
|
|
922
|
+
function getRegister(code, presetOrCustom) {
|
|
923
|
+
const card = getLanguageCard(code);
|
|
924
|
+
|
|
925
|
+
if (!card) {
|
|
926
|
+
// No card — return custom text if provided, or generic fallback
|
|
927
|
+
return presetOrCustom || DEFAULT_REGISTER_FALLBACK;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Guard: cards without register presets (e.g., ISO 639-3 stubs from
|
|
931
|
+
// the expanded language-cards directory) should fall through gracefully
|
|
932
|
+
// instead of crashing on Object.keys(null).
|
|
933
|
+
if (!card.registers) {
|
|
934
|
+
return presetOrCustom || DEFAULT_REGISTER_FALLBACK;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// No user override — use card's default preset
|
|
938
|
+
if (!presetOrCustom) {
|
|
939
|
+
const defaultKey = card.formality?.default || Object.keys(card.registers)[0];
|
|
940
|
+
return card.registers[defaultKey]?.prompt || DEFAULT_REGISTER_FALLBACK;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Check if it's a preset name
|
|
944
|
+
if (card.registers[presetOrCustom]) {
|
|
945
|
+
return card.registers[presetOrCustom].prompt;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Not a preset name — treat as custom register text (pass-through)
|
|
949
|
+
return presetOrCustom;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Get all available register presets for a locale.
|
|
954
|
+
*
|
|
955
|
+
* Returns an array of { key, label, description, prompt, isDefault } objects
|
|
956
|
+
* for use in the CLI wizard and status display.
|
|
957
|
+
*
|
|
958
|
+
* @param {string} code - Locale code
|
|
959
|
+
* @returns {Array<{ key: string, label: string, description: string, prompt: string, isDefault: boolean }>}
|
|
960
|
+
*/
|
|
961
|
+
function getRegisterPresets(code) {
|
|
962
|
+
const card = getLanguageCard(code);
|
|
963
|
+
if (!card || !card.registers) return [];
|
|
964
|
+
|
|
965
|
+
const defaultKey = card.formality?.default || Object.keys(card.registers)[0];
|
|
966
|
+
|
|
967
|
+
return Object.entries(card.registers).map(([key, preset]) => ({
|
|
968
|
+
key,
|
|
969
|
+
label: preset.label,
|
|
970
|
+
description: preset.description,
|
|
971
|
+
prompt: preset.prompt,
|
|
972
|
+
isDefault: key === defaultKey,
|
|
973
|
+
}));
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Get structured formality metadata for a locale.
|
|
978
|
+
*
|
|
979
|
+
* Used by DeepL and other methods that need to know the formality level
|
|
980
|
+
* without parsing register text strings. Returns the default formality
|
|
981
|
+
* level (e.g., 'formal', 'polite', 'neutral') or null.
|
|
982
|
+
*
|
|
983
|
+
* @param {string} code - Locale code
|
|
984
|
+
* @param {string|null} [presetOrCustom] - Active preset name or custom text
|
|
985
|
+
* @returns {{ system: string, level: string, description: string }|null}
|
|
986
|
+
*/
|
|
987
|
+
function getFormality(code, presetOrCustom) {
|
|
988
|
+
const card = getLanguageCard(code);
|
|
989
|
+
if (!card || !card.formality) return null;
|
|
990
|
+
|
|
991
|
+
// Determine which preset is active
|
|
992
|
+
const activeKey = (presetOrCustom && card.registers[presetOrCustom])
|
|
993
|
+
? presetOrCustom
|
|
994
|
+
: card.formality.default;
|
|
995
|
+
|
|
996
|
+
return {
|
|
997
|
+
system: card.formality.system,
|
|
998
|
+
level: activeKey,
|
|
999
|
+
description: card.formality.description,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Get gender-inclusive guidance for a locale.
|
|
1005
|
+
*
|
|
1006
|
+
* Separate from register so it can be appended to any register preset
|
|
1007
|
+
* without duplication.
|
|
1008
|
+
*
|
|
1009
|
+
* @param {string} code - Locale code
|
|
1010
|
+
* @returns {string|null} Gender-inclusive guidance text, or null
|
|
1011
|
+
*/
|
|
1012
|
+
function getGenderGuidance(code) {
|
|
1013
|
+
const card = getLanguageCard(code);
|
|
1014
|
+
return card?.gender?.inclusiveGuidance || null;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Get all loaded language codes (primary, not aliases).
|
|
1019
|
+
*
|
|
1020
|
+
* Repo mode: every card in shared/language-cards/ (the full catalogue).
|
|
1021
|
+
* Packaged mode: bundled core cards plus whatever the per-user cache
|
|
1022
|
+
* has materialized — i.e. the codes getLanguageCard() can serve without
|
|
1023
|
+
* touching the network. Deliberately NOT the full manifest: callers
|
|
1024
|
+
* iterate this list and fetch each card (validate.js, doctor), which
|
|
1025
|
+
* must never turn into thousands of network round-trips.
|
|
1026
|
+
*
|
|
1027
|
+
* @returns {string[]} Array of primary locale codes
|
|
1028
|
+
*/
|
|
1029
|
+
function getAllLanguageCodes() {
|
|
1030
|
+
if (_mode !== 'packaged') return Array.from(_cards.keys());
|
|
1031
|
+
const codes = new Set(_cards.keys());
|
|
1032
|
+
for (const code of listCachedCodes()) codes.add(code);
|
|
1033
|
+
return Array.from(codes);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Get method support flags for a locale.
|
|
1038
|
+
*
|
|
1039
|
+
* @param {string} code - Locale code
|
|
1040
|
+
* @returns {object|null} Method support flags, or null if no card
|
|
1041
|
+
*/
|
|
1042
|
+
function getMethodSupport(code) {
|
|
1043
|
+
const card = getLanguageCard(code);
|
|
1044
|
+
return card?.methodSupport || null;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// -----------------------------------------------------------------
|
|
1048
|
+
// Reference tier — REMOVED (v6: unified cards)
|
|
1049
|
+
// -----------------------------------------------------------------
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Get the full language reference for a locale.
|
|
1053
|
+
*
|
|
1054
|
+
* In v6 (unified cards), this is now just an alias for getLanguageCard().
|
|
1055
|
+
* All reference data (linguisticChallenges, encyclopedic, resources) is
|
|
1056
|
+
* merged directly into the runtime card files.
|
|
1057
|
+
*
|
|
1058
|
+
* Kept for backward compatibility with consumers that called
|
|
1059
|
+
* getLanguageReference() for enriched data.
|
|
1060
|
+
*
|
|
1061
|
+
* @param {string} code - Locale code (e.g., 'fr', 'ko')
|
|
1062
|
+
* @returns {object|null} Complete language card, or null if no card
|
|
1063
|
+
* @deprecated Use getLanguageCard() instead — it returns the same data.
|
|
1064
|
+
*/
|
|
1065
|
+
function getLanguageReference(code) {
|
|
1066
|
+
return getLanguageCard(code);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// -----------------------------------------------------------------
|
|
1070
|
+
// Backward-compatible DEFAULT_REGISTERS proxy
|
|
1071
|
+
// -----------------------------------------------------------------
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Backward-compatible DEFAULT_REGISTERS export.
|
|
1075
|
+
*
|
|
1076
|
+
* Returns the same shape as the old flat dictionary so all 21 consumer
|
|
1077
|
+
* call sites keep working during migration:
|
|
1078
|
+
* { name: string, register: string, dir?: string, scripts?: string }
|
|
1079
|
+
*
|
|
1080
|
+
* New code should use getLanguageCard() and getRegister() instead.
|
|
1081
|
+
*
|
|
1082
|
+
* WHY a Proxy: We don't want to maintain two data sources. The proxy
|
|
1083
|
+
* dynamically builds the old shape from the language card data on access.
|
|
1084
|
+
* The `ownKeys` trap ensures Object.keys(), Object.entries(), and
|
|
1085
|
+
* for...in loops work correctly.
|
|
1086
|
+
*/
|
|
1087
|
+
const DEFAULT_REGISTERS = new Proxy({}, {
|
|
1088
|
+
get(_target, code) {
|
|
1089
|
+
if (typeof code !== 'string') return undefined;
|
|
1090
|
+
|
|
1091
|
+
// Handle common Proxy/inspection traps
|
|
1092
|
+
if (code === Symbol.toPrimitive || code === Symbol.iterator) return undefined;
|
|
1093
|
+
if (code === 'toJSON') {
|
|
1094
|
+
// Support JSON.stringify(DEFAULT_REGISTERS)
|
|
1095
|
+
return () => {
|
|
1096
|
+
const result = {};
|
|
1097
|
+
for (const key of _cards.keys()) {
|
|
1098
|
+
result[key] = DEFAULT_REGISTERS[key];
|
|
1099
|
+
}
|
|
1100
|
+
// Also include aliases that map to different keys for backward compat
|
|
1101
|
+
// The old code had 'no' as a direct entry, now it's an alias to 'nb'
|
|
1102
|
+
return result;
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const card = getLanguageCard(code);
|
|
1107
|
+
if (!card) return undefined;
|
|
1108
|
+
|
|
1109
|
+
const defaultRegister = getRegister(code);
|
|
1110
|
+
const entry = {
|
|
1111
|
+
name: card.name,
|
|
1112
|
+
register: defaultRegister,
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
// Only include dir if RTL (matches old behavior — LTR was implied)
|
|
1116
|
+
// `textDirection` is CLDR's own layout.orientation.characterOrder,
|
|
1117
|
+
// projected as a fact. The card no longer carries a bare `dir`.
|
|
1118
|
+
if (card.textDirection === 'right-to-left' || card.dir === 'rtl') {
|
|
1119
|
+
entry.dir = 'rtl';
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Map scriptConverter to the old 'scripts' field name
|
|
1123
|
+
if (card.scriptConverter) {
|
|
1124
|
+
entry.scripts = card.scriptConverter;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
return entry;
|
|
1128
|
+
},
|
|
1129
|
+
|
|
1130
|
+
has(_target, code) {
|
|
1131
|
+
if (typeof code !== 'string') return false;
|
|
1132
|
+
return getLanguageCard(code) !== null;
|
|
1133
|
+
},
|
|
1134
|
+
|
|
1135
|
+
ownKeys() {
|
|
1136
|
+
// Return all locally-available primary codes + aliases for full
|
|
1137
|
+
// backward compat. The old code had 'no' as a key, now it's an
|
|
1138
|
+
// alias to 'nb'. (Set: Proxy ownKeys must not contain duplicates.)
|
|
1139
|
+
const keys = new Set(getAllLanguageCodes());
|
|
1140
|
+
for (const [alias] of _aliases) {
|
|
1141
|
+
keys.add(alias);
|
|
1142
|
+
}
|
|
1143
|
+
return Array.from(keys);
|
|
1144
|
+
},
|
|
1145
|
+
|
|
1146
|
+
getOwnPropertyDescriptor(_target, code) {
|
|
1147
|
+
if (typeof code !== 'string') return undefined;
|
|
1148
|
+
const card = getLanguageCard(code);
|
|
1149
|
+
if (!card) return undefined;
|
|
1150
|
+
return {
|
|
1151
|
+
configurable: true,
|
|
1152
|
+
enumerable: true,
|
|
1153
|
+
value: DEFAULT_REGISTERS[code],
|
|
1154
|
+
};
|
|
1155
|
+
},
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
// -----------------------------------------------------------------
|
|
1159
|
+
// Exports
|
|
1160
|
+
// -----------------------------------------------------------------
|
|
1161
|
+
|
|
1162
|
+
export {
|
|
1163
|
+
// New API — use these in new code
|
|
1164
|
+
getLanguageCard,
|
|
1165
|
+
getLanguageReference,
|
|
1166
|
+
getRegister,
|
|
1167
|
+
getRegisterPresets,
|
|
1168
|
+
getFormality,
|
|
1169
|
+
getGenderGuidance,
|
|
1170
|
+
getAllLanguageCodes,
|
|
1171
|
+
getMethodSupport,
|
|
1172
|
+
resolveCode,
|
|
1173
|
+
DEFAULT_REGISTER_FALLBACK,
|
|
1174
|
+
|
|
1175
|
+
// Dynamic card tier (packaged installs)
|
|
1176
|
+
prefetchLanguageCards,
|
|
1177
|
+
getCardSourceInfo,
|
|
1178
|
+
|
|
1179
|
+
// Backward-compatible export — existing consumers use this
|
|
1180
|
+
DEFAULT_REGISTERS,
|
|
1181
|
+
|
|
1182
|
+
// Internal — for testing
|
|
1183
|
+
CARDS_DIR,
|
|
1184
|
+
REFERENCE_DIR,
|
|
1185
|
+
};
|