champollion 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -26
- package/bin/cli.js +53 -5
- package/index.js +63 -2
- package/lib/api-key.js +17 -4
- package/lib/autofix.js +83 -36
- package/lib/bridge/method_bridge.py +15 -3
- package/lib/cards/reader.js +34 -0
- package/lib/cards/remote.js +15 -0
- package/lib/cards/search-names.js +178 -0
- package/lib/command-help.js +286 -85
- package/lib/commands/audit.js +10 -3
- package/lib/commands/card.js +583 -226
- package/lib/commands/doctor.js +54 -18
- package/lib/commands/help.js +37 -32
- package/lib/commands/init.js +1689 -87
- package/lib/commands/integrity.js +127 -40
- package/lib/commands/leaderboard.js +187 -67
- package/lib/commands/models.js +9 -2
- package/lib/commands/provenance.js +7 -2
- package/lib/commands/recommend.js +43 -14
- package/lib/commands/register-corpus.js +632 -125
- package/lib/commands/seal-corpus.js +1 -1
- package/lib/commands/status.js +564 -27
- package/lib/commands/submit.js +17 -12
- package/lib/commands/sync.js +31 -7
- package/lib/commands/tm.js +15 -9
- package/lib/commands/verify.js +27 -3
- package/lib/commands/wrap.js +63 -5
- package/lib/commands/xliff.js +135 -64
- package/lib/commercial-eligibility.js +1 -1
- package/lib/config.js +196 -14
- package/lib/content-estimate.js +96 -0
- package/lib/content-refusals.js +270 -0
- package/lib/content-review.js +372 -0
- package/lib/content-sync.js +1127 -344
- package/lib/content.js +94 -7
- package/lib/corpus-registration.mjs +194 -35
- package/lib/cost-label.js +29 -0
- package/lib/cost-report.js +726 -78
- package/lib/diff.js +38 -4
- package/lib/docusaurus-sync.js +965 -253
- package/lib/edit-distance.js +31 -0
- package/lib/fallback.js +964 -0
- package/lib/file-scope.js +106 -0
- package/lib/flatten.js +80 -3
- package/lib/flutter-locales.js +124 -0
- package/lib/format.js +266 -12
- package/lib/hash.js +146 -21
- package/lib/icu-structure.js +929 -0
- package/lib/integrity.js +223 -75
- package/lib/language-pair.js +157 -0
- package/lib/lint.js +78 -16
- package/lib/local-only-marks.js +106 -0
- package/lib/locale-layout.js +1103 -0
- package/lib/locale-state.js +571 -0
- package/lib/methods/anthropic.js +5 -0
- package/lib/methods/apertium.js +6 -3
- package/lib/methods/api.js +138 -25
- package/lib/methods/base.js +17 -0
- package/lib/methods/coaching-data.js +153 -0
- package/lib/methods/content-separator.js +43 -0
- package/lib/methods/deepl.js +1 -1
- package/lib/methods/direct-llm.js +252 -103
- package/lib/methods/external.js +146 -63
- package/lib/methods/gemini.js +1 -0
- package/lib/methods/google-translate.js +1 -0
- package/lib/methods/http-utils.js +41 -0
- package/lib/methods/libretranslate.js +7 -2
- package/lib/methods/llm-coached.js +68 -128
- package/lib/methods/llm.js +80 -31
- package/lib/methods/local.js +93 -10
- package/lib/methods/microsoft-translator.js +1 -2
- package/lib/methods/openai.js +4 -2
- package/lib/methods/openrouter-client.js +20 -19
- package/lib/methods/openrouter-pricing.js +150 -13
- package/lib/methods/prompt-methods.js +20 -0
- package/lib/methods/provider-pricing.js +42 -1
- package/lib/methods/request-capture.js +104 -0
- package/lib/methods/tilde.js +1 -1
- package/lib/methods/translated.js +1 -2
- package/lib/missing-key.js +93 -0
- package/lib/models.js +11 -0
- package/lib/name-rules.js +32 -0
- package/lib/named-keys.js +172 -0
- package/lib/no-translate.js +4 -3
- package/lib/output.js +160 -19
- package/lib/pairs.js +586 -30
- package/lib/placeholders.js +394 -0
- package/lib/plugins.js +8 -0
- package/lib/plural-gap-redo.js +109 -0
- package/lib/plurals.js +323 -0
- package/lib/po.js +1187 -0
- package/lib/public-catalogue.js +74 -0
- package/lib/recommend.js +527 -32
- package/lib/redo.js +95 -0
- package/lib/refusal-category.js +44 -0
- package/lib/registers.js +255 -11
- package/lib/repair-script.js +20 -13
- package/lib/scripts.js +6 -1
- package/lib/seal.mjs +4 -3
- package/lib/sealed-qualifier.mjs +1 -1
- package/lib/segment.js +2 -1
- package/lib/seo.js +19 -9
- package/lib/serve.js +43 -6
- package/lib/shared-output-seed.js +164 -0
- package/lib/source-contexts.js +39 -0
- package/lib/submit.mjs +57 -5
- package/lib/sync.js +2923 -474
- package/lib/terminology.js +13 -4
- package/lib/tm-evict.js +179 -0
- package/lib/tm-seed.js +5 -2
- package/lib/tm.js +818 -36
- package/lib/translate-pair.js +639 -34
- package/lib/translate.js +78 -5
- package/lib/types.js +22 -3
- package/lib/validate.js +880 -17
- package/lib/verify.js +1296 -104
- package/lib/watch.js +32 -13
- package/lib/xliff.js +44 -3
- package/package.json +1 -1
- package/shared/CORPORA-CARDS.md +2 -0
- package/shared/cards-fallback.json +1 -1
- package/shared/curated-orthography-conventions.json +26 -8
- package/shared/gettext-plural-forms.json +45 -0
- package/shared/method-registry.json +2 -0
- package/shared/metric-registry.json +96 -18
- package/shared/schemas/champollion-plugin.schema.json +4 -0
- package/shared/schemas/corpora-card.schema.json +8 -2
- package/shared/schemas/method-index-record.schema.json +67 -0
- package/shared/schemas/method-registry.schema.json +4 -0
- package/shared/schemas/metric-registry.schema.json +55 -1
- package/shared/docent/corpus.json +0 -11739
package/lib/commands/status.js
CHANGED
|
@@ -7,15 +7,418 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { resolveConfig, autoDetectLanguages } from '../config.js';
|
|
10
|
+
import { discoverLocaleLayout, loadSourceUnits, expectedForTarget, readLocaleFlat, lockKey } from '../locale-layout.js';
|
|
11
|
+
import { readLock } from '../hash.js';
|
|
12
|
+
import { LockState, localeHealth, countReplacedEdits, REPLACED_EDITS_FILENAME, decodeWritten, valueHash } from '../locale-state.js';
|
|
13
|
+
import { originKey } from '../plurals.js';
|
|
14
|
+
import { redoCommand } from '../verify.js';
|
|
15
|
+
import { tmProofTextsFor } from '../tm-evict.js';
|
|
10
16
|
import { resolvePairs, QUALITY_TIERS } from '../pairs.js';
|
|
11
17
|
import { loadPlugins, resolvePluginForPair } from '../plugins.js';
|
|
12
|
-
import { DEFAULT_REGISTERS, getLanguageCard, getRegisterPresets, DEFAULT_REGISTER_FALLBACK } from '../registers.js';
|
|
13
|
-
import { loadTM, tmSize, TM_DIR, TM_FILENAME } from '../tm.js';
|
|
18
|
+
import { DEFAULT_REGISTERS, getLanguageCard, getRegisterPresets, DEFAULT_REGISTER_FALLBACK, summarizeGenderGuidance } from '../registers.js';
|
|
19
|
+
import { loadTM, tmSize, modelsBehindValues, tmMethodKey, canonicalWriterKey, fallbackWriterOf, describeMethodKey, adoptLegacyCoachingKeys, TM_DIR, TM_FILENAME } from '../tm.js';
|
|
20
|
+
import { COACHING_PROMPT_METHODS } from '../methods/prompt-methods.js';
|
|
14
21
|
import { getMethod } from '../translate.js';
|
|
22
|
+
import { contentStatus } from '../content-sync.js';
|
|
23
|
+
import { PLAIN_LLM_METHODS } from '../methods/llm.js';
|
|
15
24
|
import { output } from '../output.js';
|
|
25
|
+
import { FALLBACK_MAJORITY_SHARE } from '../fallback.js';
|
|
16
26
|
import fs from 'node:fs';
|
|
17
27
|
import path from 'node:path';
|
|
18
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Did the config set this pair's quality tier? A pair resolved without one
|
|
31
|
+
* carries the default label in `_defaults` (lib/pairs.js); a pair object
|
|
32
|
+
* without that record (older callers) counts as set unless it is the
|
|
33
|
+
* default, so nothing a person wrote is hidden.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} pairConfig
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
function qualityTierSet(pairConfig) {
|
|
39
|
+
if (!pairConfig.qualityTier) return false;
|
|
40
|
+
if (pairConfig._defaults instanceof Set) return !pairConfig._defaults.has('qualityTier');
|
|
41
|
+
return pairConfig.qualityTier !== 'standard';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Methods that run a model the config names (the LLM methods). */
|
|
45
|
+
function runsModel(method) {
|
|
46
|
+
return PLAIN_LLM_METHODS.has(method) || method === 'llm-coached';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The model a pair (or a fallback) will actually use: direct providers may
|
|
51
|
+
* carry model=null (the method class picks its own default at runtime).
|
|
52
|
+
* null for a method that runs no model the config names — `api` (the model
|
|
53
|
+
* is whatever its endpoint serves) and the MT engines (deepl, google-translate…),
|
|
54
|
+
* which used to be shown "model: auto".
|
|
55
|
+
*/
|
|
56
|
+
function displayModelFor(pairConfig) {
|
|
57
|
+
if (!runsModel(pairConfig.method)) return null;
|
|
58
|
+
if (pairConfig.model) return pairConfig.model;
|
|
59
|
+
// llm-coached on a direct provider runs that provider's default model.
|
|
60
|
+
const transport = pairConfig.method === 'llm-coached' && pairConfig.provider && pairConfig.provider !== 'openrouter'
|
|
61
|
+
? pairConfig.provider
|
|
62
|
+
: pairConfig.method;
|
|
63
|
+
try {
|
|
64
|
+
return getMethod(transport)._getDefaultModel?.() || 'auto';
|
|
65
|
+
} catch {
|
|
66
|
+
return 'auto';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The header's model/endpoint summary. "Default model" only when a pair
|
|
72
|
+
* actually runs the top-level model (or, with no pairs yet, when the
|
|
73
|
+
* default method runs one): an api-only project printed the built-in
|
|
74
|
+
* OpenRouter default, a model nothing in it uses. The endpoints of `api`
|
|
75
|
+
* pairs are listed instead.
|
|
76
|
+
*
|
|
77
|
+
* @returns {{ defaultModel: string|null, endpoints: string[] }}
|
|
78
|
+
*/
|
|
79
|
+
function headerModels(config, pairs) {
|
|
80
|
+
const list = [...pairs.values()];
|
|
81
|
+
const usesDefault = list.length === 0
|
|
82
|
+
? runsModel(config.defaultMethod || 'llm')
|
|
83
|
+
: list.some(p => displayModelFor(p) === config.model);
|
|
84
|
+
const endpoints = [...new Set(list.filter(p => p.method === 'api' && p.endpoint).map(p => p.endpoint))];
|
|
85
|
+
return { defaultModel: usesDefault ? config.model : null, endpoints };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Methods that run a model the user picks — its licence is theirs to check. */
|
|
89
|
+
const MODEL_YOU_CHOOSE = new Set(['local', 'api', 'external']);
|
|
90
|
+
const MODEL_YOU_CHOOSE_NOTE = 'runs a model you choose — Champollion cannot check its licence; make sure your use of it is allowed';
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Per target locale: which models produced the values its files hold now
|
|
94
|
+
* (lib/tm.js modelsBehindValues). Empty when there is no cache, or the
|
|
95
|
+
* layout cannot be read — status is a diagnostic and never fails on it.
|
|
96
|
+
*
|
|
97
|
+
* @returns {Map<string, Array<{ model: string, keys: number, current: boolean }>>}
|
|
98
|
+
*/
|
|
99
|
+
function modelsInFiles(config, pairs, cwd) {
|
|
100
|
+
const out = new Map();
|
|
101
|
+
if (config.format === 'docusaurus') return out;
|
|
102
|
+
const hasTM = fs.existsSync(path.join(cwd, TM_DIR, TM_FILENAME));
|
|
103
|
+
let layout;
|
|
104
|
+
let units;
|
|
105
|
+
let state;
|
|
106
|
+
try {
|
|
107
|
+
layout = discoverLocaleLayout(config, { cwd });
|
|
108
|
+
units = loadSourceUnits(layout);
|
|
109
|
+
state = new LockState(readLock(cwd).locales);
|
|
110
|
+
} catch {
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
if (!hasTM && Object.values(state.locales).every(l => Object.keys(l.by || {}).length === 0)) return out;
|
|
114
|
+
const tm = hasTM ? loadTM(cwd) : {};
|
|
115
|
+
// A cache from before coaching was keyed reads as sync reads it (in memory).
|
|
116
|
+
adoptLegacyCoachingKeys(tm, pairs.values());
|
|
117
|
+
for (const [, pairConfig] of pairs) {
|
|
118
|
+
const items = [];
|
|
119
|
+
const localeState = state.peek(pairConfig.target);
|
|
120
|
+
for (const unit of units) {
|
|
121
|
+
let file;
|
|
122
|
+
try { file = layout.fileFor(pairConfig.target, unit.ns); } catch { continue; }
|
|
123
|
+
if (!file || !fs.existsSync(file.path)) continue;
|
|
124
|
+
let target;
|
|
125
|
+
try { target = readLocaleFlat(file); } catch { continue; }
|
|
126
|
+
const { flat: expected, expansion } = expectedForTarget(unit, config.inputLocale, pairConfig.target);
|
|
127
|
+
for (const [key, src] of Object.entries(expected)) {
|
|
128
|
+
if (typeof src !== 'string' || typeof target[key] !== 'string') continue;
|
|
129
|
+
// The lock's record of who wrote the value, while it is still the
|
|
130
|
+
// value on disk (a hand edit since makes it no longer apply).
|
|
131
|
+
const lk = lockKey(layout, unit.ns, key);
|
|
132
|
+
const record = decodeWritten(localeState.written[lk]);
|
|
133
|
+
const by = record && record.value === valueHash(target[key]) ? (localeState.by[lk] || null) : null;
|
|
134
|
+
// A key from before coaching was keyed, for this same setup, reads
|
|
135
|
+
// as today's (lib/tm.js canonicalWriterKey).
|
|
136
|
+
const writtenBy = by ? canonicalWriterKey(tm, pairConfig.target, by) : null;
|
|
137
|
+
items.push({ texts: tmProofTextsFor(key, src, expansion), value: target[key], writtenBy });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
out.set(pairConfig.target, modelsBehindValues(tm, pairConfig, items));
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Per target locale (pairs with a fallback): how many values in its files the
|
|
147
|
+
* fallback method wrote — the lock's `by` record naming the fallback's method
|
|
148
|
+
* key, while the value is still the one sync wrote. The files themselves do
|
|
149
|
+
* not say which strings came from the fallback (Round 8, school + hospital
|
|
150
|
+
* personas). Key-value files only; empty when nothing can be read.
|
|
151
|
+
*
|
|
152
|
+
* `earlier`: values the fallback wrote as it was set up BEFORE (another
|
|
153
|
+
* model, register or coaching of it — lib/tm.js fallbackWriterOf), which a
|
|
154
|
+
* redo re-translates (Round 10, school persona: after a coaching file was
|
|
155
|
+
* added to the fallback, nothing showed that its values predate it).
|
|
156
|
+
*
|
|
157
|
+
* `written`: every value sync wrote that is still as it wrote it (any
|
|
158
|
+
* method) — the whole the fallback's share is a part of (Round 11, school
|
|
159
|
+
* persona: status said "8 value(s)" with nothing to measure it against).
|
|
160
|
+
*
|
|
161
|
+
* @returns {Map<string, { count: number, keys: string[], earlier: { count: number, keys: string[], by: string[] }, written: number }>}
|
|
162
|
+
*/
|
|
163
|
+
function fallbackValuesInFiles(config, pairs, cwd) {
|
|
164
|
+
const out = new Map();
|
|
165
|
+
if (config.format === 'docusaurus') return out;
|
|
166
|
+
if (![...pairs.values()].some(pc => pc.fallback)) return out;
|
|
167
|
+
let layout;
|
|
168
|
+
let units;
|
|
169
|
+
let state;
|
|
170
|
+
try {
|
|
171
|
+
layout = discoverLocaleLayout(config, { cwd });
|
|
172
|
+
units = loadSourceUnits(layout);
|
|
173
|
+
state = new LockState(readLock(cwd).locales);
|
|
174
|
+
} catch {
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
const tm = fs.existsSync(path.join(cwd, TM_DIR, TM_FILENAME)) ? loadTM(cwd) : {};
|
|
178
|
+
adoptLegacyCoachingKeys(tm, pairs.values());
|
|
179
|
+
for (const [, pairConfig] of pairs) {
|
|
180
|
+
if (!pairConfig.fallback) continue;
|
|
181
|
+
const localeState = state.peek(pairConfig.target);
|
|
182
|
+
const keys = [];
|
|
183
|
+
const earlier = { count: 0, keys: [], by: [] };
|
|
184
|
+
let written = 0;
|
|
185
|
+
for (const unit of units) {
|
|
186
|
+
let file;
|
|
187
|
+
try { file = layout.fileFor(pairConfig.target, unit.ns); } catch { continue; }
|
|
188
|
+
if (!file || !fs.existsSync(file.path)) continue;
|
|
189
|
+
let target;
|
|
190
|
+
try { target = readLocaleFlat(file); } catch { continue; }
|
|
191
|
+
const { flat: expected } = expectedForTarget(unit, config.inputLocale, pairConfig.target);
|
|
192
|
+
for (const key of Object.keys(expected)) {
|
|
193
|
+
const lk = lockKey(layout, unit.ns, key);
|
|
194
|
+
if (typeof target[key] !== 'string') continue;
|
|
195
|
+
const record = decodeWritten(localeState.written[lk]);
|
|
196
|
+
if (!record || record.value !== valueHash(target[key])) continue;
|
|
197
|
+
// A value sync wrote that is still as written: the share's whole.
|
|
198
|
+
written++;
|
|
199
|
+
const by = localeState.by?.[lk];
|
|
200
|
+
if (!by) continue;
|
|
201
|
+
const writer = fallbackWriterOf(pairConfig, canonicalWriterKey(tm, pairConfig.target, by));
|
|
202
|
+
if (!writer) continue;
|
|
203
|
+
if (writer === 'current') keys.push(lk);
|
|
204
|
+
else {
|
|
205
|
+
earlier.keys.push(lk);
|
|
206
|
+
if (!earlier.by.includes(by)) earlier.by.push(by);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
earlier.count = earlier.keys.length;
|
|
211
|
+
out.set(pairConfig.target, { count: keys.length, keys, earlier, written });
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The status line for a locale whose files hold text from a model other than
|
|
218
|
+
* the current one — a mix of two models, or ALL of it from an earlier model
|
|
219
|
+
* (Round 4, Next.js persona: only the mixed case was reported) — or null.
|
|
220
|
+
*/
|
|
221
|
+
function mixedModelsLine(pairKey, models, currentModel = null) {
|
|
222
|
+
if (!models || models.length === 0) return null;
|
|
223
|
+
const unify = `champollion sync --pair ${pairKey} --redo all --fresh-on-model-change`;
|
|
224
|
+
const plural = (n) => `${n} key${n === 1 ? '' : 's'}`;
|
|
225
|
+
if (models.length === 1) {
|
|
226
|
+
const only = models[0];
|
|
227
|
+
if (only.current) return null;
|
|
228
|
+
if (only.unknown) {
|
|
229
|
+
return ` model unknown: ${plural(only.keys)} hold text that ${only.candidates.join(' and ')} cached identically, so which of them wrote it `
|
|
230
|
+
+ `cannot be told${currentModel ? ` (the current model is ${currentModel})` : ''}. To have the current model translate them: ${unify}`;
|
|
231
|
+
}
|
|
232
|
+
// Both directions: a one-off `sync --model` run wrote them, and the user
|
|
233
|
+
// may want to keep that model (Round 10, Next.js persona).
|
|
234
|
+
return ` earlier model: every translation in the files that can be attributed (${plural(only.keys)}) `
|
|
235
|
+
+ `came from ${only.model}${currentModel ? `, not the current model (${currentModel})` : ''}. To have the current model translate them: ${unify}`
|
|
236
|
+
+ ` — or, to keep ${only.model}'s text, make it the model: "model": "${only.model}" in champollion.config.json (nothing is sent)`;
|
|
237
|
+
}
|
|
238
|
+
const list = models.map(m => (m.unknown
|
|
239
|
+
? `model unknown (${plural(m.keys)}: ${m.candidates.join(' and ')} cached the same text)`
|
|
240
|
+
: `${m.model} (${plural(m.keys)}${m.current ? ', current' : ''})`)).join(', ');
|
|
241
|
+
const known = models.filter(m => !m.unknown).length;
|
|
242
|
+
return ` mixed: the files hold text from ${known > 1 ? `${known} models` : 'more than one model'} — ${list}. To have the current model translate the ones an earlier model wrote: ${unify} (what the current model already translated comes from the cache)`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Per target locale: keys a redo left pending, translations made from an
|
|
247
|
+
* older source text (out of date), and keys held back because the quality
|
|
248
|
+
* gate refused this method's translation (lib/locale-state.js). Empty for a
|
|
249
|
+
* locale with nothing to say; status never fails on it.
|
|
250
|
+
*
|
|
251
|
+
* @returns {Map<string, { stale: string[], pending: Array<{key: string, reason: string, held: boolean}>, held: string[] }>}
|
|
252
|
+
*/
|
|
253
|
+
function localeHealthMap(config, pairs, cwd) {
|
|
254
|
+
const out = new Map();
|
|
255
|
+
if (config.format === 'docusaurus') return out;
|
|
256
|
+
let layout;
|
|
257
|
+
let units;
|
|
258
|
+
let lock;
|
|
259
|
+
try {
|
|
260
|
+
layout = discoverLocaleLayout(config, { cwd });
|
|
261
|
+
units = loadSourceUnits(layout);
|
|
262
|
+
lock = readLock(cwd);
|
|
263
|
+
} catch {
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
const state = new LockState(lock.locales);
|
|
267
|
+
const tm = fs.existsSync(path.join(cwd, TM_DIR, TM_FILENAME)) ? loadTM(cwd) : null;
|
|
268
|
+
for (const [, pairConfig] of pairs) {
|
|
269
|
+
try {
|
|
270
|
+
out.set(pairConfig.target, localeHealth({
|
|
271
|
+
layout, units, inputLocale: config.inputLocale, code: pairConfig.target,
|
|
272
|
+
localeState: state.peek(pairConfig.target), manifest: lock.source, tm, pairConfig,
|
|
273
|
+
helpers: { expectedForTarget, readLocaleFlat, lockKey, originKey, fallbackPrefix: config.fallbackPrefix },
|
|
274
|
+
}));
|
|
275
|
+
} catch { /* a diagnostic: never fails */ }
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* One line on a pair's gender guidance, or null when there is none to say.
|
|
282
|
+
* @returns {string|null}
|
|
283
|
+
*/
|
|
284
|
+
function describeGenderSetting(pairConfig) {
|
|
285
|
+
const how = '"genderGuidance" in champollion.config.json changes it (false = none, or your own words)';
|
|
286
|
+
if (pairConfig.genderGuidanceSource === 'off') return `gender: no guidance (set off in the config) — ${how}`;
|
|
287
|
+
const text = summarizeGenderGuidance(pairConfig.genderGuidance);
|
|
288
|
+
if (!text) return null;
|
|
289
|
+
const from = pairConfig.genderGuidanceSource === 'config' ? 'from your config' : 'Champollion\'s default for this language';
|
|
290
|
+
return `gender (${from}; LLM methods): ${text} — ${how}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** "a, b, c, +2 more" */
|
|
294
|
+
function sampleKeys(keys, n = 5) {
|
|
295
|
+
const shown = keys.slice(0, n).map(k => String(k).replace(/\u0004/g, '\u2404'));
|
|
296
|
+
return `${shown.join(', ')}${keys.length > n ? `, +${keys.length - n} more` : ''}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Status lines for one locale's health (pending / out of date / held back). */
|
|
300
|
+
function healthLines(pairKey, health) {
|
|
301
|
+
if (!health) return [];
|
|
302
|
+
const lines = [];
|
|
303
|
+
const pendingHeld = health.pending.filter(p => p.held);
|
|
304
|
+
const pendingNext = health.pending.filter(p => !p.held);
|
|
305
|
+
if (pendingNext.length > 0) {
|
|
306
|
+
const reasons = [...new Set(pendingNext.map(p => p.reason))].join('`, `');
|
|
307
|
+
lines.push(` pending: ${pendingNext.length} key(s) an earlier \`${reasons}\` could not finish (${sampleKeys(pendingNext.map(p => p.key))}) `
|
|
308
|
+
+ '— the next `champollion sync` asks the model for them once more.');
|
|
309
|
+
}
|
|
310
|
+
if (pendingHeld.length > 0) {
|
|
311
|
+
const keys = pendingHeld.map(p => p.key);
|
|
312
|
+
lines.push(` pending, held back: ${keys.length} key(s) a redo could not finish, refused again on the retry (${sampleKeys(keys)}) `
|
|
313
|
+
+ `— not re-sent on a plain sync. Ask again: \`${redoCommand(keys.slice(0, 8), { pair: pairKey })}\`, or fill them `
|
|
314
|
+
+ '(a "fallback" method, "noTranslate", or by hand).');
|
|
315
|
+
}
|
|
316
|
+
if (health.held.length > 0) {
|
|
317
|
+
lines.push(` held back: ${health.held.length} key(s) the quality gate refused from this method (${sampleKeys(health.held)}) `
|
|
318
|
+
+ `— not re-sent on a plain sync. Ask again: \`${redoCommand(health.held.slice(0, 8), { pair: pairKey })}\`, or fill them `
|
|
319
|
+
+ '(a "fallback" method, "noTranslate", or by hand).');
|
|
320
|
+
}
|
|
321
|
+
if (health.stale.length > 0) {
|
|
322
|
+
lines.push(` out of date: ${health.stale.length} translation(s) were made from an older source text (${sampleKeys(health.stale)}) `
|
|
323
|
+
+ `— \`champollion sync --pair ${pairKey}\` re-translates them${health.held.some(k => health.stale.includes(k)) ? ' (the held-back ones only when named)' : ''}.`);
|
|
324
|
+
}
|
|
325
|
+
return lines;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Where an OpenAI-compatible method (local, openai) sends its requests, and
|
|
330
|
+
* the setting that chose it — LOCAL_API_BASE in the environment or in .env,
|
|
331
|
+
* or the built-in default (Ollama) — exactly as a connection error names it
|
|
332
|
+
* (Round 7, i18next persona: status did not say which address `local` would
|
|
333
|
+
* call). null for a method with no configurable endpoint.
|
|
334
|
+
*
|
|
335
|
+
* @returns {{ url: string, from: string|null, line: string }|null}
|
|
336
|
+
*/
|
|
337
|
+
function resolvedEndpoint(pairConfig, cwd) {
|
|
338
|
+
if (pairConfig.method === 'api') return null; // its endpoint is the pair's own, shown as [API]
|
|
339
|
+
let method;
|
|
340
|
+
try { method = getMethod(pairConfig.method, pairConfig); } catch { return null; }
|
|
341
|
+
if (typeof method?._describeEndpoint !== 'function' || typeof method._resolveApiBaseSource !== 'function') return null;
|
|
342
|
+
let line = null;
|
|
343
|
+
let src = null;
|
|
344
|
+
try {
|
|
345
|
+
line = method._describeEndpoint({ cwd, ...(pairConfig.baseUrl ? { baseUrl: pairConfig.baseUrl } : {}) });
|
|
346
|
+
src = method._resolveApiBaseSource({ cwd, ...(pairConfig.baseUrl ? { baseUrl: pairConfig.baseUrl } : {}) });
|
|
347
|
+
} catch { return null; }
|
|
348
|
+
if (!line || !src?.base) return null;
|
|
349
|
+
return { url: src.base, from: src.from || null, line };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** The content lane for status (null without a contentDir). */
|
|
353
|
+
function contentState(config, pairs, cwd) {
|
|
354
|
+
if (!config.contentDir || config.format === 'docusaurus') return null;
|
|
355
|
+
try {
|
|
356
|
+
return contentStatus(config.contentDir, config.inputLocale, [...pairs.entries()], cwd, { fallbackPrefix: config.fallbackPrefix || '[EN] ' });
|
|
357
|
+
} catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Human lines for the content lane: the folder, its files, each locale's state. */
|
|
363
|
+
function contentLines(config, pairs, cwd) {
|
|
364
|
+
const st = contentState(config, pairs, cwd);
|
|
365
|
+
if (!st) return [];
|
|
366
|
+
const lines = [` Content (Markdown): ${st.dir}/ — ${st.files} source file(s), each translated beside it as <name>.<locale>.md\n`];
|
|
367
|
+
const few = (list) => `${list.slice(0, 3).join(', ')}${list.length > 3 ? `, +${list.length - 3} more` : ''}`;
|
|
368
|
+
for (const [code, l] of Object.entries(st.locales)) {
|
|
369
|
+
const parts = [`${l.translated} translated`];
|
|
370
|
+
if (l.outOfDate.length > 0) parts.push(`${l.outOfDate.length} out of date (${few(l.outOfDate)})`);
|
|
371
|
+
if (l.pending.length > 0) parts.push(`${l.pending.length} pending (${few(l.pending)})`);
|
|
372
|
+
if (l.unrecorded.length > 0) parts.push(`${l.unrecorded.length} with no record — kept as they are (${few(l.unrecorded)})`);
|
|
373
|
+
const todo = l.outOfDate.length + l.pending.length;
|
|
374
|
+
lines.push(` ${code}: ${parts.join(', ')}${todo > 0 ? ' — `champollion sync` translates the out-of-date and pending ones' : ''}`);
|
|
375
|
+
}
|
|
376
|
+
lines.push('');
|
|
377
|
+
return lines;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The coaching a pair (or a fallback) carries, for status: where it comes
|
|
382
|
+
* from, and the fingerprint its cache key holds (the "coaching …" that sync's
|
|
383
|
+
* notes name) — or null when it has none. A method that sends no coaching
|
|
384
|
+
* says so: the file changes nothing there (Round 10, school persona: status
|
|
385
|
+
* did not show the fallback's coaching at all).
|
|
386
|
+
*
|
|
387
|
+
* @returns {{ file: string|null, inline: boolean, fingerprint: string|null, sent: boolean }|null}
|
|
388
|
+
*/
|
|
389
|
+
function coachingOf(pairConfig) {
|
|
390
|
+
const text = typeof pairConfig.coachingPrompt === 'string' && pairConfig.coachingPrompt.trim() ? pairConfig.coachingPrompt : null;
|
|
391
|
+
const file = typeof pairConfig.coachingFile === 'string' && pairConfig.coachingFile.trim() ? pairConfig.coachingFile : null;
|
|
392
|
+
if (!text && !file) return null;
|
|
393
|
+
const sent = COACHING_PROMPT_METHODS.has(pairConfig.method);
|
|
394
|
+
const fingerprint = sent ? (tmMethodKey(pairConfig).split('|')[3] || null) : null;
|
|
395
|
+
return { file, inline: !file, fingerprint, sent };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** "coaching: coaching.json (key f430948b)", or the reason it is not sent. */
|
|
399
|
+
function coachingLabel(pairConfig) {
|
|
400
|
+
const c = coachingOf(pairConfig);
|
|
401
|
+
if (!c) return null;
|
|
402
|
+
const what = c.file ? c.file : 'inline coachingPrompt';
|
|
403
|
+
return c.sent
|
|
404
|
+
? `coaching: ${what}${c.fingerprint ? ` (cache key coaching ${c.fingerprint})` : ''}`
|
|
405
|
+
: `coaching: ${what} — not sent: ${pairConfig.method} takes no coaching`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** One line describing a pair's fallback, for the human status display. */
|
|
409
|
+
function describeFallback(fallback, cwd, pairConfig = null) {
|
|
410
|
+
const model = displayModelFor(fallback);
|
|
411
|
+
const resolved = resolvedEndpoint(fallback, cwd);
|
|
412
|
+
const where = fallback.method === 'api' && fallback.endpoint ? ` | endpoint: ${fallback.endpoint}`
|
|
413
|
+
: resolved ? ` | endpoint: ${resolved.line}` : '';
|
|
414
|
+
const coaching = coachingLabel(fallback);
|
|
415
|
+
// Its register only when it is not the pair's (a fallback inherits it).
|
|
416
|
+
const register = pairConfig && (fallback.register !== pairConfig.register || fallback.registerPreset !== pairConfig.registerPreset)
|
|
417
|
+
? ` | register: ${fallback.registerPreset || JSON.stringify(String(fallback.register || '').slice(0, 40))}` : '';
|
|
418
|
+
return ` fallback: ${fallback.method}${model ? ` | model: ${model}` : ''}${where}${coaching ? ` | ${coaching}` : ''}${register}`
|
|
419
|
+
+ ' (translates what the method above cannot translate safely)';
|
|
420
|
+
}
|
|
421
|
+
|
|
19
422
|
// CLI version — read from package.json (the single source of truth), never a
|
|
20
423
|
// hardcoded literal. Mirrors bin/cli.js's --version read.
|
|
21
424
|
const { version: CLI_VERSION } = JSON.parse(
|
|
@@ -42,7 +445,7 @@ async function run(args, cwd) {
|
|
|
42
445
|
}
|
|
43
446
|
config.resolvedLanguages = languages;
|
|
44
447
|
|
|
45
|
-
const pairs = resolvePairs(config);
|
|
448
|
+
const pairs = resolvePairs(config, { cwd });
|
|
46
449
|
|
|
47
450
|
// Load installed plugins to enrich status display
|
|
48
451
|
const plugins = loadPlugins(cwd);
|
|
@@ -54,7 +457,19 @@ async function run(args, cwd) {
|
|
|
54
457
|
output.raw('\n champollion v3 — Translation Status\n');
|
|
55
458
|
output.raw(` Input locale: ${config.inputLocale}`);
|
|
56
459
|
output.raw(` Locales dir: ${config.localesDir}`);
|
|
57
|
-
|
|
460
|
+
// Multi-file layouts say which files make up a language — the question an
|
|
461
|
+
// i18next user asks first. Flat projects print exactly what they did.
|
|
462
|
+
if (config.format !== 'docusaurus') {
|
|
463
|
+
const layout = discoverLocaleLayout(config, { cwd });
|
|
464
|
+
if (layout.kind !== 'flat') {
|
|
465
|
+
output.raw(` Layout: ${layout.kind} — ${layout.display} (${layout.sourceFiles.length} source file(s))`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const header = headerModels(config, pairs);
|
|
469
|
+
if (header.defaultModel) output.raw(` Default model: ${header.defaultModel}`);
|
|
470
|
+
if (header.endpoints.length > 0) {
|
|
471
|
+
output.raw(` ${header.endpoints.length === 1 ? 'Endpoint: ' : 'Endpoints: '} ${header.endpoints.join(', ')}`);
|
|
472
|
+
}
|
|
58
473
|
if (config.temperature != null) {
|
|
59
474
|
output.raw(` Temperature: ${config.temperature}`);
|
|
60
475
|
}
|
|
@@ -78,6 +493,10 @@ async function run(args, cwd) {
|
|
|
78
493
|
} else {
|
|
79
494
|
output.raw(' TM cache: empty (will populate on first sync)');
|
|
80
495
|
}
|
|
496
|
+
const replaced = countReplacedEdits(cwd);
|
|
497
|
+
if (replaced > 0) {
|
|
498
|
+
output.raw(` Replaced edits: ${replaced} hand-edited translation(s) a sync replaced, with their wording, in ${REPLACED_EDITS_FILENAME}`);
|
|
499
|
+
}
|
|
81
500
|
output.raw('');
|
|
82
501
|
|
|
83
502
|
if (pairs.size === 0) {
|
|
@@ -85,6 +504,9 @@ async function run(args, cwd) {
|
|
|
85
504
|
output.raw(' Run `champollion init` or add languages to your config.\n');
|
|
86
505
|
} else {
|
|
87
506
|
output.raw(` Translation Pairs (${pairs.size}):\n`);
|
|
507
|
+
const mixed = modelsInFiles(config, pairs, cwd);
|
|
508
|
+
const health = localeHealthMap(config, pairs, cwd);
|
|
509
|
+
const fromFallback = fallbackValuesInFiles(config, pairs, cwd);
|
|
88
510
|
for (const [pairKey, pairConfig] of pairs) {
|
|
89
511
|
const tier = QUALITY_TIERS[pairConfig.qualityTier];
|
|
90
512
|
const tierLabel = tier ? tier.label : pairConfig.qualityTier;
|
|
@@ -102,17 +524,16 @@ async function run(args, cwd) {
|
|
|
102
524
|
|
|
103
525
|
// Resolve display model — direct providers may have model=null in the
|
|
104
526
|
// pair config, meaning the method class picks its own default at runtime.
|
|
105
|
-
// Show the resolved model so users know what will actually be used
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const method = getMethod(pairConfig.method);
|
|
110
|
-
displayModel = method._getDefaultModel?.() || 'auto';
|
|
111
|
-
} catch { displayModel = 'auto'; }
|
|
112
|
-
}
|
|
113
|
-
const modelStr = pairConfig.method === 'api' ? '' : ` | model: ${displayModel || 'auto'}`;
|
|
527
|
+
// Show the resolved model so users know what will actually be used; a
|
|
528
|
+
// method with no model of ours (api, the MT engines) shows none.
|
|
529
|
+
const displayModel = displayModelFor(pairConfig);
|
|
530
|
+
const modelStr = displayModel ? ` | model: ${displayModel}` : '';
|
|
114
531
|
|
|
115
|
-
//
|
|
532
|
+
// A plugin's benchmarks, when it publishes them; a quality tier only
|
|
533
|
+
// when the config sets one — and then said for what it is: a label
|
|
534
|
+
// someone chose, not a measurement. "quality: Standard" was printed for
|
|
535
|
+
// every pair, from a default nobody set, with no word on what it meant
|
|
536
|
+
// (Round 14, Next.js persona).
|
|
116
537
|
let qualityStr = '';
|
|
117
538
|
if (pairConfig.methodPlugin) {
|
|
118
539
|
const resolvedPair = resolvePluginForPair(plugins, pairConfig);
|
|
@@ -124,12 +545,44 @@ async function run(args, cwd) {
|
|
|
124
545
|
if (bm.exact_match_rate) parts.push(`exact ${Math.round(bm.exact_match_rate * 100)}%`);
|
|
125
546
|
qualityStr = ` | benchmarks: ${parts.join(', ')}`;
|
|
126
547
|
} else {
|
|
127
|
-
qualityStr =
|
|
548
|
+
qualityStr = ' | benchmarks: none in the plugin\'s manifest';
|
|
128
549
|
}
|
|
129
|
-
} else {
|
|
130
|
-
qualityStr = ` | quality: ${tierLabel}`;
|
|
131
550
|
}
|
|
132
551
|
output.raw(` method: ${pairConfig.method}${modelStr}${qualityStr}`);
|
|
552
|
+
if (qualityTierSet(pairConfig)) {
|
|
553
|
+
output.raw(` quality tier: ${tierLabel} — the "qualityTier" your config sets for this pair: a label you chose `
|
|
554
|
+
+ `(${Object.keys(QUALITY_TIERS).join(', ')}), not a measurement. Sync translates the same whatever it says; \`serve\` advertises it.`);
|
|
555
|
+
}
|
|
556
|
+
const endpoint = resolvedEndpoint(pairConfig, cwd);
|
|
557
|
+
if (endpoint) output.raw(` endpoint: ${endpoint.line}`);
|
|
558
|
+
const ownCoaching = coachingLabel(pairConfig);
|
|
559
|
+
if (ownCoaching) output.raw(` ${ownCoaching}`);
|
|
560
|
+
if (pairConfig.fallback) {
|
|
561
|
+
output.raw(describeFallback(pairConfig.fallback, cwd, pairConfig));
|
|
562
|
+
const fb = fromFallback.get(pairConfig.target);
|
|
563
|
+
const few = (keys) => keys.slice(0, 5).map(k => String(k).replace(/\u0004/g, '\u2404')).join(', ') + (keys.length > 5 ? `, +${keys.length - 5} more` : '');
|
|
564
|
+
if (fb && fb.count > 0) {
|
|
565
|
+
// The share of what sync wrote, and — above FALLBACK_MAJORITY_SHARE —
|
|
566
|
+
// that what ships is mostly the fallback's (as sync warns).
|
|
567
|
+
const share = fb.written > 0 ? fb.count / fb.written : null;
|
|
568
|
+
output.raw(` from the fallback: ${fb.count} value(s) in the files (${few(fb.keys)})`
|
|
569
|
+
+ (share !== null ? ` — ${fb.count} of the ${fb.written} sync wrote (${Math.round(share * 100)}%)` : '')
|
|
570
|
+
+ (share !== null && share > FALLBACK_MAJORITY_SHARE
|
|
571
|
+
? `: most of this locale's text is the fallback's (${pairConfig.fallback.method}), not ${pairConfig.method}'s`
|
|
572
|
+
: ''));
|
|
573
|
+
}
|
|
574
|
+
if (fb && fb.earlier?.count > 0) {
|
|
575
|
+
output.raw(` from the fallback as it was set up before: ${fb.earlier.count} value(s) (${few(fb.earlier.keys)}) — written by `
|
|
576
|
+
+ `${fb.earlier.by.map(describeMethodKey).join('; ')}, not by the fallback as it is now (${describeMethodKey(tmMethodKey(pairConfig.fallback))}). `
|
|
577
|
+
+ `A change of the fallback re-translates nothing on its own; to re-translate them: champollion sync --pair ${pairKey} --redo all`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// Said once by the first sync; here every time it is asked for.
|
|
581
|
+
if (MODEL_YOU_CHOOSE.has(pairConfig.method)) output.raw(` licence: ${MODEL_YOU_CHOOSE_NOTE}`);
|
|
582
|
+
// After a model switch without a full re-translation (Round 3).
|
|
583
|
+
const mixedLine = mixedModelsLine(pairKey, mixed.get(pairConfig.target), displayModel);
|
|
584
|
+
if (mixedLine) output.raw(mixedLine);
|
|
585
|
+
for (const line of healthLines(pairKey, health.get(pairConfig.target))) output.raw(line);
|
|
133
586
|
|
|
134
587
|
// Plugin badge — show name and version (benchmarks already shown above)
|
|
135
588
|
if (pairConfig.methodPlugin) {
|
|
@@ -143,7 +596,14 @@ async function run(args, cwd) {
|
|
|
143
596
|
|
|
144
597
|
// API badge
|
|
145
598
|
if (pairConfig.method === 'api') {
|
|
146
|
-
|
|
599
|
+
// Say where it runs: a loopback endpoint (nmt-forge serve, champollion
|
|
600
|
+
// serve) is this machine, not a remote service.
|
|
601
|
+
let host = '';
|
|
602
|
+
try { host = new URL(pairConfig.endpoint || '').hostname; } catch { /* no/invalid endpoint */ }
|
|
603
|
+
const local = ['localhost', '127.0.0.1', '::1', '[::1]'].includes(host);
|
|
604
|
+
output.raw(local
|
|
605
|
+
? ` [API] Translation runs on this machine (${pairConfig.endpoint})`
|
|
606
|
+
: ` [API] Translation runs on the endpoint's server${pairConfig.endpoint ? ` (${pairConfig.endpoint})` : ''} — the text is sent there`);
|
|
147
607
|
}
|
|
148
608
|
|
|
149
609
|
// Google Translate badge
|
|
@@ -153,6 +613,10 @@ async function run(args, cwd) {
|
|
|
153
613
|
}
|
|
154
614
|
output.raw('');
|
|
155
615
|
|
|
616
|
+
// The content lane (contentDir: Markdown pages), beside the key-value
|
|
617
|
+
// lane above — what each locale holds of it.
|
|
618
|
+
for (const line of contentLines(config, pairs, cwd)) output.raw(line);
|
|
619
|
+
|
|
156
620
|
// Show active registers for each pair with structured card info
|
|
157
621
|
output.raw(' Registers:\n');
|
|
158
622
|
for (const [pairKey, pairConfig] of pairs) {
|
|
@@ -176,6 +640,11 @@ async function run(args, cwd) {
|
|
|
176
640
|
const truncated = regText.length > 55 ? regText.slice(0, 52) + '...' : regText;
|
|
177
641
|
output.raw(` ${pairConfig.target} (custom)${systemLabel} — "${truncated}"`);
|
|
178
642
|
}
|
|
643
|
+
// The gender guidance the prompt carries (LLM methods): visible, with
|
|
644
|
+
// where it comes from and how to change it (Round 8, Django persona:
|
|
645
|
+
// the French prompt asked for écriture inclusive and nothing said so).
|
|
646
|
+
const genderLine = describeGenderSetting(pairConfig);
|
|
647
|
+
if (genderLine) output.raw(` ${genderLine}`);
|
|
179
648
|
}
|
|
180
649
|
output.raw('');
|
|
181
650
|
}
|
|
@@ -197,19 +666,16 @@ function runJson(config, pairs, plugins, cwd) {
|
|
|
197
666
|
}
|
|
198
667
|
|
|
199
668
|
const pairList = [];
|
|
669
|
+
const mixed = modelsInFiles(config, pairs, cwd);
|
|
670
|
+
const health = localeHealthMap(config, pairs, cwd);
|
|
671
|
+
const fromFallback = fallbackValuesInFiles(config, pairs, cwd);
|
|
200
672
|
for (const [pairKey, pairConfig] of pairs) {
|
|
201
673
|
const tier = QUALITY_TIERS[pairConfig.qualityTier];
|
|
202
674
|
const resolvedPair = resolvePluginForPair(plugins, pairConfig);
|
|
203
675
|
|
|
204
676
|
// Same display-model resolution as the human path: direct providers may
|
|
205
677
|
// have model=null (the method class picks its own default at runtime).
|
|
206
|
-
|
|
207
|
-
if (!displayModel && pairConfig.method !== 'api') {
|
|
208
|
-
try {
|
|
209
|
-
const method = getMethod(pairConfig.method);
|
|
210
|
-
displayModel = method._getDefaultModel?.() || 'auto';
|
|
211
|
-
} catch { displayModel = 'auto'; }
|
|
212
|
-
}
|
|
678
|
+
const displayModel = displayModelFor(pairConfig);
|
|
213
679
|
|
|
214
680
|
const presets = getRegisterPresets(pairConfig.target);
|
|
215
681
|
const matchedPreset = pairConfig.registerPreset
|
|
@@ -221,9 +687,20 @@ function runJson(config, pairs, plugins, cwd) {
|
|
|
221
687
|
target: pairConfig.target,
|
|
222
688
|
name: pairConfig.name,
|
|
223
689
|
method: pairConfig.method,
|
|
224
|
-
|
|
690
|
+
// null for a method that runs no model of ours (api, the MT engines).
|
|
691
|
+
model: displayModel,
|
|
692
|
+
// Where an `api` pair sends its text (null for every other method).
|
|
693
|
+
endpoint: pairConfig.method === 'api' ? (pairConfig.endpoint || null) : null,
|
|
694
|
+
// Its coaching: the file (null for inline text), the fingerprint its
|
|
695
|
+
// cache key holds, and whether the method sends it (null: none).
|
|
696
|
+
coaching: coachingOf(pairConfig),
|
|
697
|
+
// Where an OpenAI-compatible method (local, openai) sends its requests
|
|
698
|
+
// and the setting that chose it ({ url, from }; absent for other methods).
|
|
699
|
+
...(() => { const r = resolvedEndpoint(pairConfig, cwd); return r ? { requestsGoTo: { url: r.url, from: r.from } } : {}; })(),
|
|
225
700
|
qualityTier: pairConfig.qualityTier,
|
|
226
701
|
qualityTierLabel: tier ? tier.label : pairConfig.qualityTier,
|
|
702
|
+
// Whether the config sets it (false: the default label, a claim nobody made).
|
|
703
|
+
qualityTierSet: qualityTierSet(pairConfig),
|
|
227
704
|
dir: pairConfig.dir || 'ltr',
|
|
228
705
|
scripts: pairConfig.scripts || null,
|
|
229
706
|
// Resolved script decision (kept alongside legacy `scripts` for compat)
|
|
@@ -241,22 +718,82 @@ function runJson(config, pairs, plugins, cwd) {
|
|
|
241
718
|
register: matchedPreset
|
|
242
719
|
? { preset: matchedPreset.key, label: matchedPreset.label, isDefault: !!matchedPreset.isDefault }
|
|
243
720
|
: { preset: null, custom: pairConfig.register || DEFAULT_REGISTER_FALLBACK },
|
|
721
|
+
// The gender guidance LLM prompts carry: catalogue (the default for the
|
|
722
|
+
// language), config (your own), off — and its text.
|
|
723
|
+
genderGuidance: { source: pairConfig.genderGuidanceSource || null, text: pairConfig.genderGuidance || null },
|
|
724
|
+
// Models whose cached output the files hold now (most keys first);
|
|
725
|
+
// more than one = mixed after a model switch.
|
|
726
|
+
modelsInFiles: mixed.get(pairConfig.target) || [],
|
|
727
|
+
// Every attributable value came from a model other than the current one.
|
|
728
|
+
fromEarlierModel: (() => {
|
|
729
|
+
const m = mixed.get(pairConfig.target) || [];
|
|
730
|
+
return m.length === 1 && !m[0].current && !m[0].unknown ? m[0].model : null;
|
|
731
|
+
})(),
|
|
732
|
+
// lib/locale-state.js: keys an unfinished redo left (asked again by the
|
|
733
|
+
// next sync unless held), translations of an older source text, and
|
|
734
|
+
// keys held back after a quality-gate refusal.
|
|
735
|
+
pending: health.get(pairConfig.target)?.pending || [],
|
|
736
|
+
stale: health.get(pairConfig.target)?.stale || [],
|
|
737
|
+
heldBack: health.get(pairConfig.target)?.held || [],
|
|
738
|
+
// A model the user picks: its licence is theirs to check (null otherwise).
|
|
739
|
+
licenceNote: MODEL_YOU_CHOOSE.has(pairConfig.method) ? MODEL_YOU_CHOOSE_NOTE : null,
|
|
740
|
+
// The second method for what this pair's own method cannot translate
|
|
741
|
+
// safely (null when none is configured).
|
|
742
|
+
fallback: pairConfig.fallback
|
|
743
|
+
? {
|
|
744
|
+
method: pairConfig.fallback.method,
|
|
745
|
+
model: displayModelFor(pairConfig.fallback),
|
|
746
|
+
provider: pairConfig.fallback.provider || null,
|
|
747
|
+
endpoint: pairConfig.fallback.endpoint || null,
|
|
748
|
+
...(() => { const r = resolvedEndpoint(pairConfig.fallback, cwd); return r ? { requestsGoTo: { url: r.url, from: r.from } } : {}; })(),
|
|
749
|
+
// Its coaching, as for the pair; its register only when not the pair's.
|
|
750
|
+
coaching: coachingOf(pairConfig.fallback),
|
|
751
|
+
...((pairConfig.fallback.register !== pairConfig.register || pairConfig.fallback.registerPreset !== pairConfig.registerPreset)
|
|
752
|
+
&& { register: pairConfig.fallback.registerPreset ? { preset: pairConfig.fallback.registerPreset } : { custom: pairConfig.fallback.register || null } }),
|
|
753
|
+
// Values in the files this fallback wrote (the lock's record), and which.
|
|
754
|
+
...(fromFallback.has(pairConfig.target) && { valuesInFiles: fromFallback.get(pairConfig.target).count }),
|
|
755
|
+
// Of how many values sync wrote (any method), and the share.
|
|
756
|
+
...(fromFallback.get(pairConfig.target)?.written > 0 && {
|
|
757
|
+
valuesWritten: fromFallback.get(pairConfig.target).written,
|
|
758
|
+
share: fromFallback.get(pairConfig.target).count / fromFallback.get(pairConfig.target).written,
|
|
759
|
+
}),
|
|
760
|
+
...(fromFallback.get(pairConfig.target)?.count > 0 && { keys: fromFallback.get(pairConfig.target).keys }),
|
|
761
|
+
// Values it wrote as it was set up before (another model, register
|
|
762
|
+
// or coaching of it): `sync --redo all` re-translates them.
|
|
763
|
+
...(fromFallback.get(pairConfig.target)?.earlier?.count > 0 && {
|
|
764
|
+
earlierSetup: {
|
|
765
|
+
values: fromFallback.get(pairConfig.target).earlier.count,
|
|
766
|
+
keys: fromFallback.get(pairConfig.target).earlier.keys,
|
|
767
|
+
writtenBy: fromFallback.get(pairConfig.target).earlier.by.map(describeMethodKey),
|
|
768
|
+
},
|
|
769
|
+
}),
|
|
770
|
+
}
|
|
771
|
+
: null,
|
|
244
772
|
});
|
|
245
773
|
}
|
|
246
774
|
|
|
775
|
+
const header = headerModels(config, pairs);
|
|
247
776
|
console.log(JSON.stringify({
|
|
248
777
|
command: 'status',
|
|
249
778
|
version: CLI_VERSION,
|
|
250
779
|
inputLocale: config.inputLocale,
|
|
251
780
|
localesDir: config.localesDir,
|
|
252
|
-
|
|
781
|
+
// The top-level model — null when no pair runs it (an api-only project).
|
|
782
|
+
defaultModel: header.defaultModel,
|
|
783
|
+
// The endpoints of `api` pairs (what they run is whatever those serve).
|
|
784
|
+
endpoints: header.endpoints,
|
|
253
785
|
temperature: config.temperature ?? null,
|
|
254
786
|
configVersion: config.version || '2 (legacy)',
|
|
255
787
|
format: config.format,
|
|
256
788
|
contentDir: config.contentDir || null,
|
|
257
789
|
plugins: plugins.size,
|
|
258
790
|
tm,
|
|
791
|
+
replacedEdits: { count: countReplacedEdits(cwd), file: REPLACED_EDITS_FILENAME },
|
|
259
792
|
pairs: pairList,
|
|
793
|
+
// The content lane (contentDir): the folder, its source files, and per
|
|
794
|
+
// locale how many translations are current, out of date, pending, or
|
|
795
|
+
// unrecorded (null without a contentDir).
|
|
796
|
+
content: contentState(config, pairs, cwd),
|
|
260
797
|
}, null, 2));
|
|
261
798
|
|
|
262
799
|
return 0;
|