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/pairs.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language pair resolution — converts config into a directional pair graph.
|
|
3
|
+
*
|
|
4
|
+
* WHY: champollion v2 assumed English→X for everything. v3 models
|
|
5
|
+
* translation as directional pairs (en:fr, es:en, en:crk) where each
|
|
6
|
+
* pair can have its own method, model, quality tier, and cost profile.
|
|
7
|
+
*
|
|
8
|
+
* The pair model supports two config modes:
|
|
9
|
+
* 1. Simple: `languages: ["fr", "de"]` — all pairs use default method/model
|
|
10
|
+
* 2. Advanced: `pairs: { "en:crk": { method: "fst-gated" } }` — per-pair overrides
|
|
11
|
+
*
|
|
12
|
+
* Both can coexist: `pairs` overrides `languages` for specific language targets.
|
|
13
|
+
*
|
|
14
|
+
* PAIR KEY FORMAT: "source:target" (e.g., "en:fr")
|
|
15
|
+
* Canonical separator is the colon (:) — compact, ASCII-safe, and
|
|
16
|
+
* unambiguous since no locale code contains a colon. Legacy formats
|
|
17
|
+
* (→, ->) are still accepted by parsePairKey for backward compatibility.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { DEFAULT_REGISTERS, getLanguageCard, resolveCode, DEFAULT_REGISTER_FALLBACK } from './registers.js';
|
|
21
|
+
import { resolveTargetScript, validateScriptFallback, converterKeyForLocale } from './scripts.js';
|
|
22
|
+
import { getMethod } from './translate.js';
|
|
23
|
+
import { DEFAULT_OPENROUTER_MODEL, DEFAULT_BATCH_SIZE, DEFAULT_MAX_RETRIES } from './config.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Quality tiers — define the expected reliability of a translation.
|
|
27
|
+
*
|
|
28
|
+
* These tiers aren't arbitrary labels. Each corresponds to a concrete
|
|
29
|
+
* verification level that determines how much the output can be trusted
|
|
30
|
+
* without human review.
|
|
31
|
+
*/
|
|
32
|
+
const QUALITY_TIERS = {
|
|
33
|
+
standard: {
|
|
34
|
+
label: 'Standard',
|
|
35
|
+
description: 'Direct LLM translation. No post-processing verification.',
|
|
36
|
+
},
|
|
37
|
+
high: {
|
|
38
|
+
label: 'High',
|
|
39
|
+
description: 'LLM translation with grammar/dictionary coaching. Better for complex morphology.',
|
|
40
|
+
},
|
|
41
|
+
research: {
|
|
42
|
+
label: 'Research',
|
|
43
|
+
description: 'LLM + deterministic FST/grammar gate. Morphologically verified output.',
|
|
44
|
+
},
|
|
45
|
+
verified: {
|
|
46
|
+
label: 'Verified',
|
|
47
|
+
description: 'LLM draft flagged for human review. Highest confidence.',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Default method config — applied to all pairs unless overridden.
|
|
53
|
+
*/
|
|
54
|
+
const PAIR_DEFAULTS = {
|
|
55
|
+
method: 'llm',
|
|
56
|
+
model: null, // null = inherit from top-level config.model
|
|
57
|
+
qualityTier: 'standard',
|
|
58
|
+
batchSize: null, // null = inherit from top-level config.batchSize
|
|
59
|
+
maxRetries: DEFAULT_MAX_RETRIES, // max cascade retries on batch parse failure (batch → half → individual)
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Methods that connect directly to a provider API (not via OpenRouter).
|
|
64
|
+
*
|
|
65
|
+
* For these methods, the global config.model (which is an OpenRouter slug like
|
|
66
|
+
* "google/gemini-3.5-flash") would be wrong — each provider has its own model
|
|
67
|
+
* naming scheme. When model is not explicitly set, we pass null so the method
|
|
68
|
+
* class's _getDefaultModel() fires with the correct provider-specific slug.
|
|
69
|
+
*/
|
|
70
|
+
const DIRECT_PROVIDER_METHODS = new Set([
|
|
71
|
+
'gemini', 'openai', 'anthropic', 'deepl',
|
|
72
|
+
'google-translate', 'microsoft-translator', 'libretranslate',
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the model for a translation pair.
|
|
77
|
+
*
|
|
78
|
+
* - If the user explicitly set a model, use it (they know what they want).
|
|
79
|
+
* - If the method is a direct provider, return null — let the method class
|
|
80
|
+
* pick its own default via _getDefaultModel().
|
|
81
|
+
* - Otherwise (OpenRouter-style methods), use the global default.
|
|
82
|
+
*
|
|
83
|
+
* @param {string|null} explicitModel - Model from per-language or per-pair config
|
|
84
|
+
* @param {string} method - Translation method name
|
|
85
|
+
* @param {string} globalDefault - Global model from config (OpenRouter slug)
|
|
86
|
+
* @returns {string|null} Resolved model, or null for direct providers
|
|
87
|
+
*/
|
|
88
|
+
function resolveModelForPair(explicitModel, method, globalDefault) {
|
|
89
|
+
if (explicitModel) return explicitModel;
|
|
90
|
+
if (DIRECT_PROVIDER_METHODS.has(method)) return null;
|
|
91
|
+
return globalDefault;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the full pair graph from config.
|
|
96
|
+
*
|
|
97
|
+
* Returns a Map of pairKey → pairConfig, where each pairConfig contains:
|
|
98
|
+
* - source: source locale code (e.g., "en")
|
|
99
|
+
* - target: target locale code (e.g., "fr")
|
|
100
|
+
* - method: translation method name (e.g., "llm", "llm-coached")
|
|
101
|
+
* - model: model identifier (e.g., "openai/gpt-4o-mini")
|
|
102
|
+
* - qualityTier: one of QUALITY_TIERS keys
|
|
103
|
+
* - batchSize: keys per API batch
|
|
104
|
+
* - register: target language register (tone/style instructions)
|
|
105
|
+
* - name: target language display name
|
|
106
|
+
* - dir: text directionality ('ltr' or 'rtl')
|
|
107
|
+
* - scripts: available script conversions (if any)
|
|
108
|
+
* - endpoint: API endpoint URL for the bare "api" method (if set)
|
|
109
|
+
*
|
|
110
|
+
* Pair keys use colon separator: "en:fr", "en:crk".
|
|
111
|
+
* Legacy arrow formats (en→fr, en->fr) in config.pairs are accepted
|
|
112
|
+
* by parsePairKey but stored internally in colon format.
|
|
113
|
+
*
|
|
114
|
+
* @param {import('./types.js').ChampollionConfig} config - Resolved config (post-migration, post-defaults)
|
|
115
|
+
* @returns {Map<string, import('./types.js').PairConfig>} Pair graph
|
|
116
|
+
*/
|
|
117
|
+
function resolvePairs(config) {
|
|
118
|
+
const pairs = new Map();
|
|
119
|
+
const inputLocale = config.inputLocale;
|
|
120
|
+
const defaultModel = config.model || DEFAULT_OPENROUTER_MODEL;
|
|
121
|
+
const defaultBatchSize = config.batchSize || DEFAULT_BATCH_SIZE;
|
|
122
|
+
const defaultMethod = config.defaultMethod || PAIR_DEFAULTS.method;
|
|
123
|
+
|
|
124
|
+
// Step 1: Build pairs from the `languages` array (simple mode)
|
|
125
|
+
const languages = config.resolvedLanguages || {};
|
|
126
|
+
for (const [code, langConfig] of Object.entries(languages)) {
|
|
127
|
+
const pairKey = buildPairKey(inputLocale, code);
|
|
128
|
+
// Use language card for structured metadata (formality, gender, script).
|
|
129
|
+
// Falls back to backward-compat proxy for languages without cards.
|
|
130
|
+
const card = getLanguageCard(code);
|
|
131
|
+
const registerInfo = DEFAULT_REGISTERS[code] || {};
|
|
132
|
+
|
|
133
|
+
// Resolution order: language config > global config > defaults.
|
|
134
|
+
// Language-level fields (model, batchSize, maxRetries, script) set
|
|
135
|
+
// per-language defaults without requiring the verbose `pairs` syntax.
|
|
136
|
+
//
|
|
137
|
+
// _defaults tracks which fields were filled from system defaults rather
|
|
138
|
+
// than explicitly set by the user. resolvePluginForPair uses this to
|
|
139
|
+
// let plugin config override defaults while respecting explicit settings.
|
|
140
|
+
const _defaults = new Set();
|
|
141
|
+
const method = langConfig.method || defaultMethod;
|
|
142
|
+
const model = resolveModelForPair(langConfig.model, method, defaultModel);
|
|
143
|
+
if (!langConfig.model) _defaults.add('model');
|
|
144
|
+
const batchSize = langConfig.batchSize || defaultBatchSize;
|
|
145
|
+
if (!langConfig.batchSize) _defaults.add('batchSize');
|
|
146
|
+
const register = langConfig.register || registerInfo.register || DEFAULT_REGISTER_FALLBACK;
|
|
147
|
+
if (!langConfig.register) _defaults.add('register');
|
|
148
|
+
|
|
149
|
+
// Track fields from system defaults so plugins can override them.
|
|
150
|
+
// If the user didn't explicitly set these, they're defaults.
|
|
151
|
+
const temperature = langConfig.temperature ?? config.temperature ?? null;
|
|
152
|
+
if (langConfig.temperature == null) _defaults.add('temperature');
|
|
153
|
+
const coachingFile = langConfig.coachingFile ?? config.coachingFile ?? null;
|
|
154
|
+
if (langConfig.coachingFile == null) _defaults.add('coachingFile');
|
|
155
|
+
const coachingPrompt = langConfig.coachingPrompt ?? config.coachingPrompt ?? null;
|
|
156
|
+
if (langConfig.coachingPrompt == null) _defaults.add('coachingPrompt');
|
|
157
|
+
const promptContext = langConfig.promptContext ?? config.promptContext ?? null;
|
|
158
|
+
if (langConfig.promptContext == null) _defaults.add('promptContext');
|
|
159
|
+
const contentSegmentation = langConfig.contentSegmentation ?? config.contentSegmentation ?? null;
|
|
160
|
+
if (langConfig.contentSegmentation == null) _defaults.add('contentSegmentation');
|
|
161
|
+
|
|
162
|
+
pairs.set(pairKey, {
|
|
163
|
+
source: inputLocale,
|
|
164
|
+
target: code,
|
|
165
|
+
method,
|
|
166
|
+
model,
|
|
167
|
+
qualityTier: PAIR_DEFAULTS.qualityTier,
|
|
168
|
+
batchSize,
|
|
169
|
+
maxRetries: langConfig.maxRetries ?? PAIR_DEFAULTS.maxRetries,
|
|
170
|
+
register,
|
|
171
|
+
// Preset key name for consumers that need to look up preset-specific
|
|
172
|
+
// metadata (e.g., DeepL formality mapping). null means custom text.
|
|
173
|
+
registerPreset: langConfig.registerPreset || null,
|
|
174
|
+
name: langConfig.name || registerInfo.name || code,
|
|
175
|
+
// textDirection is the projected CLDR fact; `dir` was the old field name.
|
|
176
|
+
dir: (card?.textDirection === 'right-to-left' ? 'rtl' : null)
|
|
177
|
+
|| card?.dir || registerInfo.dir || 'ltr',
|
|
178
|
+
scripts: card?.scriptConverter || registerInfo.scripts || null,
|
|
179
|
+
script: langConfig.script || null,
|
|
180
|
+
scriptFallback: langConfig.scriptFallback || null,
|
|
181
|
+
// Pair-level API endpoint for the bare "api" method (no plugin manifest).
|
|
182
|
+
// APIMethod falls back to pairConfig.endpoint — dropping it during
|
|
183
|
+
// normalization made { method: "api", endpoint: … } configs unusable.
|
|
184
|
+
endpoint: langConfig.endpoint || null,
|
|
185
|
+
// Structured formality info for method-specific behavior (e.g., DeepL)
|
|
186
|
+
formalitySystem: card?.formality?.system || null,
|
|
187
|
+
// Language-specific gender guidance for LLM prompts (e.g., écriture inclusive for French)
|
|
188
|
+
genderGuidance: card?.gender?.inclusiveGuidance || null,
|
|
189
|
+
// Global prompt context from config (e.g., "This is a developer tool README")
|
|
190
|
+
promptContext,
|
|
191
|
+
// Temperature: per-language → global config → null (method picks its own default)
|
|
192
|
+
temperature,
|
|
193
|
+
// Coaching: coaching file path and resolved prompt text
|
|
194
|
+
coachingFile,
|
|
195
|
+
coachingPrompt,
|
|
196
|
+
// Markdown body translation granularity ('block' | 'page') — consumed
|
|
197
|
+
// by docusaurus-sync.js Phase 2 (validated there, fail-loud).
|
|
198
|
+
contentSegmentation,
|
|
199
|
+
_defaults,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Step 2: Apply overrides from `pairs` object (advanced mode)
|
|
204
|
+
// These can override simple-mode pairs or add entirely new ones
|
|
205
|
+
if (config.pairs && typeof config.pairs === 'object') {
|
|
206
|
+
for (const [rawPairKey, pairOverride] of Object.entries(config.pairs)) {
|
|
207
|
+
const { source: rawSource, target: rawTarget } = parsePairKey(rawPairKey);
|
|
208
|
+
if (!rawSource || !rawTarget) {
|
|
209
|
+
console.error(`[ERR] Invalid pair key "${rawPairKey}" — expected format "source:target" (e.g., "en:fr")`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Preserve the user's raw locale code as `target` — this determines
|
|
214
|
+
// file paths and must match the user's framework (e.g., Docusaurus
|
|
215
|
+
// expects `i18n/fil/`, not `i18n/tl/`).
|
|
216
|
+
//
|
|
217
|
+
// BUG FIX: Previously, resolveCode() replaced the target entirely
|
|
218
|
+
// (fil→tl), causing translations to be written to directories that
|
|
219
|
+
// the user's framework couldn't find.
|
|
220
|
+
//
|
|
221
|
+
// We don't need a separate canonical code because getLanguageCard()
|
|
222
|
+
// and DEFAULT_REGISTERS already resolve aliases internally — passing
|
|
223
|
+
// 'fil' to getLanguageCard() correctly returns the Tagalog card.
|
|
224
|
+
//
|
|
225
|
+
// IMPORTANT: Use rawSource directly, not resolveCode(rawSource).
|
|
226
|
+
// Step 1 builds keys from the raw inputLocale (e.g., 'en'). If we
|
|
227
|
+
// resolve source here (e.g., 'en' → 'eng'), the key won't match
|
|
228
|
+
// and the override won't apply to the Step 1 pair.
|
|
229
|
+
const source = rawSource;
|
|
230
|
+
const target = rawTarget;
|
|
231
|
+
const pairKey = buildPairKey(source, target);
|
|
232
|
+
const card = getLanguageCard(target);
|
|
233
|
+
const registerInfo = DEFAULT_REGISTERS[target] || {};
|
|
234
|
+
const existing = pairs.get(pairKey) || {};
|
|
235
|
+
const existingDefaults = existing._defaults || new Set();
|
|
236
|
+
|
|
237
|
+
// _defaults: a field is "defaulted" if NEITHER the pairOverride NOR
|
|
238
|
+
// the existing pair set it explicitly. If pairOverride sets a field,
|
|
239
|
+
// it clears the default flag; if it falls through to existing, it
|
|
240
|
+
// inherits that pair's default tracking.
|
|
241
|
+
const _defaults = new Set();
|
|
242
|
+
const resolvedMethod = pairOverride.method || existing.method || defaultMethod;
|
|
243
|
+
const model = resolveModelForPair(
|
|
244
|
+
pairOverride.model || existing.model,
|
|
245
|
+
resolvedMethod,
|
|
246
|
+
defaultModel
|
|
247
|
+
);
|
|
248
|
+
if (!pairOverride.model && existingDefaults.has('model')) _defaults.add('model');
|
|
249
|
+
if (!pairOverride.model && !existing.model) _defaults.add('model');
|
|
250
|
+
const batchSize = pairOverride.batchSize || existing.batchSize || defaultBatchSize;
|
|
251
|
+
if (!pairOverride.batchSize && existingDefaults.has('batchSize')) _defaults.add('batchSize');
|
|
252
|
+
if (!pairOverride.batchSize && !existing.batchSize) _defaults.add('batchSize');
|
|
253
|
+
const register = pairOverride.register || existing.register || registerInfo.register || DEFAULT_REGISTER_FALLBACK;
|
|
254
|
+
if (!pairOverride.register && existingDefaults.has('register')) _defaults.add('register');
|
|
255
|
+
if (!pairOverride.register && !existing.register) _defaults.add('register');
|
|
256
|
+
|
|
257
|
+
// Track temperature, coaching, and context fields for plugin override resolution
|
|
258
|
+
const temperature = pairOverride.temperature ?? existing.temperature ?? config.temperature ?? null;
|
|
259
|
+
if (pairOverride.temperature == null && existingDefaults.has('temperature')) _defaults.add('temperature');
|
|
260
|
+
if (pairOverride.temperature == null && existing.temperature == null) _defaults.add('temperature');
|
|
261
|
+
const coachingFile = pairOverride.coachingFile ?? existing.coachingFile ?? config.coachingFile ?? null;
|
|
262
|
+
if (pairOverride.coachingFile == null && existingDefaults.has('coachingFile')) _defaults.add('coachingFile');
|
|
263
|
+
if (pairOverride.coachingFile == null && !existing.coachingFile) _defaults.add('coachingFile');
|
|
264
|
+
const coachingPrompt = pairOverride.coachingPrompt ?? existing.coachingPrompt ?? config.coachingPrompt ?? null;
|
|
265
|
+
if (pairOverride.coachingPrompt == null && existingDefaults.has('coachingPrompt')) _defaults.add('coachingPrompt');
|
|
266
|
+
if (pairOverride.coachingPrompt == null && !existing.coachingPrompt) _defaults.add('coachingPrompt');
|
|
267
|
+
const promptContext = pairOverride.promptContext ?? existing.promptContext ?? config.promptContext ?? null;
|
|
268
|
+
if (pairOverride.promptContext == null && existingDefaults.has('promptContext')) _defaults.add('promptContext');
|
|
269
|
+
if (pairOverride.promptContext == null && !existing.promptContext) _defaults.add('promptContext');
|
|
270
|
+
const contentSegmentation = pairOverride.contentSegmentation ?? existing.contentSegmentation ?? config.contentSegmentation ?? null;
|
|
271
|
+
if (pairOverride.contentSegmentation == null && existingDefaults.has('contentSegmentation')) _defaults.add('contentSegmentation');
|
|
272
|
+
if (pairOverride.contentSegmentation == null && !existing.contentSegmentation) _defaults.add('contentSegmentation');
|
|
273
|
+
|
|
274
|
+
// Resolve the preset key for this pair. If the pair override specifies
|
|
275
|
+
// a register value, check if it's a known preset key (for DeepL, etc.).
|
|
276
|
+
// Otherwise inherit from the existing pair's preset tracking.
|
|
277
|
+
let registerPreset = existing.registerPreset || null;
|
|
278
|
+
if (pairOverride.register) {
|
|
279
|
+
// Check if the override is a preset key we should resolve
|
|
280
|
+
const isPresetKey = card?.registers?.[pairOverride.register] != null;
|
|
281
|
+
registerPreset = isPresetKey ? pairOverride.register : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
pairs.set(pairKey, {
|
|
285
|
+
source,
|
|
286
|
+
target,
|
|
287
|
+
method: resolvedMethod,
|
|
288
|
+
model,
|
|
289
|
+
qualityTier: pairOverride.qualityTier || existing.qualityTier || PAIR_DEFAULTS.qualityTier,
|
|
290
|
+
batchSize,
|
|
291
|
+
maxRetries: pairOverride.maxRetries ?? existing.maxRetries ?? PAIR_DEFAULTS.maxRetries,
|
|
292
|
+
register,
|
|
293
|
+
registerPreset,
|
|
294
|
+
name: pairOverride.name || existing.name || registerInfo.name || target,
|
|
295
|
+
dir: (card?.textDirection === 'right-to-left' ? 'rtl' : null)
|
|
296
|
+
|| card?.dir || existing.dir || registerInfo.dir || 'ltr',
|
|
297
|
+
scripts: card?.scriptConverter || existing.scripts || registerInfo.scripts || null,
|
|
298
|
+
script: pairOverride.script || existing.script || null,
|
|
299
|
+
scriptFallback: pairOverride.scriptFallback || existing.scriptFallback || null,
|
|
300
|
+
// Pair-level API endpoint for the bare "api" method (no plugin manifest).
|
|
301
|
+
// Preserved so APIMethod's documented pairConfig.endpoint fallback works.
|
|
302
|
+
endpoint: pairOverride.endpoint || existing.endpoint || null,
|
|
303
|
+
// Plugin reference — the plugin loader will merge its config into this pair
|
|
304
|
+
methodPlugin: pairOverride.methodPlugin || null,
|
|
305
|
+
formalitySystem: card?.formality?.system || existing.formalitySystem || null,
|
|
306
|
+
genderGuidance: card?.gender?.inclusiveGuidance || existing.genderGuidance || null,
|
|
307
|
+
// Global prompt context flows from config into every pair
|
|
308
|
+
promptContext,
|
|
309
|
+
temperature,
|
|
310
|
+
// Coaching: coaching file path and resolved prompt text
|
|
311
|
+
coachingFile,
|
|
312
|
+
coachingPrompt,
|
|
313
|
+
// Markdown body translation granularity ('block' | 'page')
|
|
314
|
+
contentSegmentation,
|
|
315
|
+
_defaults,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Step 3: Script resolution — ONE decision per pair, attached here so every
|
|
321
|
+
// consumer (sync, serve, docusaurus, status, integrity, repair-script)
|
|
322
|
+
// reads the same answer, and an invalid `script:` or `scriptFallback` fails
|
|
323
|
+
// at pair-graph build time — before preflight, before any API spend, and in
|
|
324
|
+
// --dry runs too. The choice-required state (crk/sr-class dual real
|
|
325
|
+
// orthographies) is NOT a throw here: read-only lanes may proceed and
|
|
326
|
+
// display it; the translation lanes refuse in resolveRuntime.
|
|
327
|
+
for (const [pairKey, pc] of pairs) {
|
|
328
|
+
const card = getLanguageCard(pc.target);
|
|
329
|
+
try {
|
|
330
|
+
pc.scriptResolution = resolveTargetScript(pc.target, pc, card);
|
|
331
|
+
if (pc.scriptFallback != null) {
|
|
332
|
+
const registered = converterKeyForLocale(pc.target, card);
|
|
333
|
+
if (!registered) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`"scriptFallback" has no effect for ${pc.target} — no script converter is registered for this locale.`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
validateScriptFallback(pc.scriptFallback, registered);
|
|
339
|
+
}
|
|
340
|
+
} catch (err) {
|
|
341
|
+
err.message = `${pairKey}: ${err.message}`;
|
|
342
|
+
throw err;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return pairs;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Parse a pair key into its source and target components.
|
|
351
|
+
*
|
|
352
|
+
* Supports three separator formats (checked in this order):
|
|
353
|
+
* 1. ":" — canonical format (e.g., "en:fr")
|
|
354
|
+
* 2. "→" — legacy Unicode arrow (e.g., "en→fr")
|
|
355
|
+
* 3. "->" — legacy ASCII arrow (e.g., "en->fr")
|
|
356
|
+
*
|
|
357
|
+
* WHY colon: Compact, ASCII-safe, and unambiguous — no locale code
|
|
358
|
+
* contains a colon, unlike underscores (pt_BR) or hyphens (zh-TW).
|
|
359
|
+
* Legacy arrow formats are accepted for backward compatibility with
|
|
360
|
+
* existing configs.
|
|
361
|
+
*
|
|
362
|
+
* @param {string} pairKey - Pair key to parse
|
|
363
|
+
* @returns {{ source: string|null, target: string|null }}
|
|
364
|
+
*/
|
|
365
|
+
function parsePairKey(pairKey) {
|
|
366
|
+
// Canonical colon first, then legacy arrow formats
|
|
367
|
+
const separators = [':', '→', '->'];
|
|
368
|
+
for (const sep of separators) {
|
|
369
|
+
const idx = pairKey.indexOf(sep);
|
|
370
|
+
if (idx !== -1) {
|
|
371
|
+
const source = pairKey.slice(0, idx).trim();
|
|
372
|
+
const target = pairKey.slice(idx + sep.length).trim();
|
|
373
|
+
if (source && target) {
|
|
374
|
+
return { source, target };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return { source: null, target: null };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Build a pair key from source and target locale codes.
|
|
383
|
+
*
|
|
384
|
+
* Uses the canonical colon separator format.
|
|
385
|
+
*
|
|
386
|
+
* @param {string} source - Source locale code
|
|
387
|
+
* @param {string} target - Target locale code
|
|
388
|
+
* @returns {string} Pair key (e.g., "en:fr")
|
|
389
|
+
*/
|
|
390
|
+
function buildPairKey(source, target) {
|
|
391
|
+
return `${source}:${target}`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Filter a resolved pair graph down to the pair(s) named by `--pair`.
|
|
396
|
+
*
|
|
397
|
+
* Accepts a comma-separated list. Each entry is matched against the
|
|
398
|
+
* CONFIGURED pair graph — a value that doesn't name a configured pair is a
|
|
399
|
+
* hard error, never a silent no-op: translating every locale when the user
|
|
400
|
+
* asked for one is a money trap, and translating none is a silent failure.
|
|
401
|
+
*
|
|
402
|
+
* Accepted spellings per entry:
|
|
403
|
+
* - "en:fr" — canonical (also legacy "en→fr" / "en->fr")
|
|
404
|
+
* - "en>fr" — the leaderboard's separator, normalized here
|
|
405
|
+
* - "en-fr" — docs shorthand; resolved by trying every hyphen split
|
|
406
|
+
* against the configured pairs (so "en-pt-BR" works too),
|
|
407
|
+
* accepted only when exactly one configured pair matches
|
|
408
|
+
*
|
|
409
|
+
* @param {string} rawFlag - The raw --pair value (e.g. "en:fr,en:de")
|
|
410
|
+
* @param {Map<string, object>} pairs - Configured pair graph from resolvePairs
|
|
411
|
+
* @returns {Map<string, object>} New Map containing only the requested pairs
|
|
412
|
+
* @throws {Error} On a malformed entry or an entry naming no configured pair
|
|
413
|
+
*/
|
|
414
|
+
function filterPairGraph(rawFlag, pairs) {
|
|
415
|
+
const configured = [...pairs.keys()].sort();
|
|
416
|
+
const configuredList = configured.length > 0 ? configured.join(', ') : '(none)';
|
|
417
|
+
|
|
418
|
+
const fail = (badValue, why) => {
|
|
419
|
+
const lines = [
|
|
420
|
+
'',
|
|
421
|
+
' ┌─ UNKNOWN PAIR ──────────────────────────────────────────────────┐',
|
|
422
|
+
' │ --pair names a pair this project does not configure. │',
|
|
423
|
+
' └──────────────────────────────────────────────────────────────────┘',
|
|
424
|
+
'',
|
|
425
|
+
` ✗ --pair ${badValue}: ${why}`,
|
|
426
|
+
'',
|
|
427
|
+
` Configured pairs: ${configuredList}`,
|
|
428
|
+
'',
|
|
429
|
+
' Next steps:',
|
|
430
|
+
' 1. Pick a configured pair: champollion sync --pair ' + (configured[0] || 'en:fr'),
|
|
431
|
+
' 2. Or add the pair to champollion.config.json ("languages" or "pairs").',
|
|
432
|
+
];
|
|
433
|
+
throw new Error(lines.join('\n'));
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const specs = String(rawFlag).split(',').map(s => s.trim()).filter(Boolean);
|
|
437
|
+
if (specs.length === 0) {
|
|
438
|
+
fail(JSON.stringify(rawFlag), 'empty value — expected "source:target" (e.g. en:fr)');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const selected = new Map();
|
|
442
|
+
for (const spec of specs) {
|
|
443
|
+
// Canonical + legacy separators first; ">" (the leaderboard separator)
|
|
444
|
+
// normalized to the canonical colon before parsing.
|
|
445
|
+
const { source, target } = parsePairKey(spec.replace('>', ':'));
|
|
446
|
+
let key = source && target ? buildPairKey(source, target) : null;
|
|
447
|
+
|
|
448
|
+
if (key && !pairs.has(key)) {
|
|
449
|
+
fail(spec, `"${key}" is not in the configured pair graph`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (!key) {
|
|
453
|
+
// Docs shorthand "en-fr": hyphens are ambiguous (pt-BR), so try every
|
|
454
|
+
// split and accept only an exact, unique match against configured pairs.
|
|
455
|
+
const candidates = [];
|
|
456
|
+
for (let i = spec.indexOf('-'); i !== -1; i = spec.indexOf('-', i + 1)) {
|
|
457
|
+
const candidate = buildPairKey(spec.slice(0, i).trim(), spec.slice(i + 1).trim());
|
|
458
|
+
if (pairs.has(candidate) && !candidates.includes(candidate)) candidates.push(candidate);
|
|
459
|
+
}
|
|
460
|
+
if (candidates.length === 1) {
|
|
461
|
+
key = candidates[0];
|
|
462
|
+
} else if (candidates.length > 1) {
|
|
463
|
+
fail(spec, `ambiguous — matches ${candidates.join(' and ')}; use the colon form`);
|
|
464
|
+
} else {
|
|
465
|
+
fail(spec, 'does not match any configured pair — expected "source:target" (e.g. en:fr)');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
selected.set(key, pairs.get(key));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return selected;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Get all target locale codes from a pair graph.
|
|
477
|
+
*
|
|
478
|
+
* @param {Map<string, object>} pairs - Pair graph
|
|
479
|
+
* @returns {string[]} Unique target locale codes
|
|
480
|
+
*/
|
|
481
|
+
function getTargetLocales(pairs) {
|
|
482
|
+
const targets = new Set();
|
|
483
|
+
for (const pair of pairs.values()) {
|
|
484
|
+
targets.add(pair.target);
|
|
485
|
+
}
|
|
486
|
+
return [...targets];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Get the pair config for a specific target locale.
|
|
491
|
+
* Searches for any pair where the target matches the given code.
|
|
492
|
+
*
|
|
493
|
+
* @param {Map<string, object>} pairs - Pair graph
|
|
494
|
+
* @param {string} targetCode - Target locale code
|
|
495
|
+
* @returns {object|null} Pair config, or null if not found
|
|
496
|
+
*/
|
|
497
|
+
function getPairForTarget(pairs, targetCode) {
|
|
498
|
+
for (const pair of pairs.values()) {
|
|
499
|
+
if (pair.target === targetCode) {
|
|
500
|
+
return pair;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Estimate the cost of translating a set of keys for a given pair.
|
|
508
|
+
*
|
|
509
|
+
* Delegates to the pair's configured method class. Each method knows
|
|
510
|
+
* its own pricing model (or honestly returns null when it can't know).
|
|
511
|
+
*
|
|
512
|
+
* @param {number} keyCount - Number of keys to translate
|
|
513
|
+
* @param {object} pairConfig - Pair config with method and model
|
|
514
|
+
* @returns {{ estimatedCost: number|null, currency: string, source: string, note: string }}
|
|
515
|
+
*/
|
|
516
|
+
async function estimateCost(keyCount, pairConfig) {
|
|
517
|
+
const methodName = pairConfig.method || 'llm';
|
|
518
|
+
|
|
519
|
+
// Delegate to the method's own cost estimate.
|
|
520
|
+
// WHY: We can't hardcode pricing here because each method has its own
|
|
521
|
+
// pricing model (or lack thereof). Google has documented rates ($20/1M chars),
|
|
522
|
+
// LLM varies by model, API is server-determined.
|
|
523
|
+
try {
|
|
524
|
+
const method = getMethod(methodName);
|
|
525
|
+
return await method.estimateCost(keyCount, pairConfig);
|
|
526
|
+
} catch {
|
|
527
|
+
// If method resolution fails, return an honest "unknown"
|
|
528
|
+
return {
|
|
529
|
+
estimatedCost: null,
|
|
530
|
+
currency: 'USD',
|
|
531
|
+
source: 'unknown',
|
|
532
|
+
note: `Could not resolve method "${methodName}" for cost estimation.`,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export {
|
|
538
|
+
resolvePairs,
|
|
539
|
+
parsePairKey,
|
|
540
|
+
buildPairKey,
|
|
541
|
+
filterPairGraph,
|
|
542
|
+
getTargetLocales,
|
|
543
|
+
getPairForTarget,
|
|
544
|
+
estimateCost,
|
|
545
|
+
QUALITY_TIERS,
|
|
546
|
+
PAIR_DEFAULTS,
|
|
547
|
+
};
|