champollion 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +133 -0
- package/README.md +387 -0
- package/bin/cli.js +278 -0
- package/index.js +135 -0
- package/lib/api-key.js +127 -0
- package/lib/autofix.js +432 -0
- package/lib/bridge/method_bridge.py +430 -0
- package/lib/card-source-resolution.mjs +284 -0
- package/lib/cards/cache.js +169 -0
- package/lib/cards/env.js +82 -0
- package/lib/cards/fetch-card-child.js +38 -0
- package/lib/cards/reader.js +435 -0
- package/lib/cards/refresh.js +111 -0
- package/lib/cards/remote.js +387 -0
- package/lib/cldf-export.mjs +540 -0
- package/lib/cldf-terms.mjs +62 -0
- package/lib/command-help.js +790 -0
- package/lib/commands/audit.js +49 -0
- package/lib/commands/card.js +454 -0
- package/lib/commands/doctor.js +559 -0
- package/lib/commands/fonts.js +489 -0
- package/lib/commands/help.js +91 -0
- package/lib/commands/init.js +1259 -0
- package/lib/commands/integrity.js +148 -0
- package/lib/commands/leaderboard.js +478 -0
- package/lib/commands/lint.js +30 -0
- package/lib/commands/models.js +177 -0
- package/lib/commands/plugin.js +103 -0
- package/lib/commands/provenance.js +45 -0
- package/lib/commands/recommend.js +75 -0
- package/lib/commands/register-corpus.js +678 -0
- package/lib/commands/repair-script.js +42 -0
- package/lib/commands/seal-corpus.js +355 -0
- package/lib/commands/seo.js +72 -0
- package/lib/commands/serve.js +147 -0
- package/lib/commands/status.js +265 -0
- package/lib/commands/submit.js +332 -0
- package/lib/commands/sync.js +89 -0
- package/lib/commands/tm.js +573 -0
- package/lib/commands/verify.js +39 -0
- package/lib/commands/watch.js +20 -0
- package/lib/commands/wrap.js +138 -0
- package/lib/commands/xliff.js +327 -0
- package/lib/commercial-eligibility.js +235 -0
- package/lib/concurrent.js +87 -0
- package/lib/config.js +523 -0
- package/lib/contamination-lane.js +76 -0
- package/lib/content-sync.js +731 -0
- package/lib/content.js +733 -0
- package/lib/corpus-registration.mjs +608 -0
- package/lib/cost-report.js +346 -0
- package/lib/diff.js +155 -0
- package/lib/docusaurus-sync.js +1256 -0
- package/lib/flatten.js +55 -0
- package/lib/format.js +954 -0
- package/lib/hash.js +159 -0
- package/lib/icu.js +473 -0
- package/lib/integrity.js +689 -0
- package/lib/license-gate.mjs +478 -0
- package/lib/license-identify.mjs +229 -0
- package/lib/lint.js +629 -0
- package/lib/method-manifest.js +60 -0
- package/lib/methods/anthropic.js +140 -0
- package/lib/methods/apertium.js +163 -0
- package/lib/methods/api.js +316 -0
- package/lib/methods/base.js +184 -0
- package/lib/methods/content-separator.js +45 -0
- package/lib/methods/deepl.js +426 -0
- package/lib/methods/direct-llm.js +586 -0
- package/lib/methods/external.js +332 -0
- package/lib/methods/fetch-with-retry.js +124 -0
- package/lib/methods/gemini.js +147 -0
- package/lib/methods/google-translate.js +402 -0
- package/lib/methods/http-utils.js +122 -0
- package/lib/methods/libretranslate.js +314 -0
- package/lib/methods/llm-coached.js +670 -0
- package/lib/methods/llm.js +592 -0
- package/lib/methods/local.js +76 -0
- package/lib/methods/microsoft-translator.js +331 -0
- package/lib/methods/openai.js +131 -0
- package/lib/methods/openrouter-client.js +327 -0
- package/lib/methods/openrouter-pricing.js +156 -0
- package/lib/methods/provider-env.js +115 -0
- package/lib/methods/provider-pricing.js +310 -0
- package/lib/methods/tilde.js +150 -0
- package/lib/methods/translated.js +229 -0
- package/lib/methods/translation-error.js +80 -0
- package/lib/models.js +258 -0
- package/lib/no-translate.js +233 -0
- package/lib/output.js +238 -0
- package/lib/pairs.js +547 -0
- package/lib/plugins.js +447 -0
- package/lib/provenance.js +323 -0
- package/lib/recommend.js +648 -0
- package/lib/registers.js +1185 -0
- package/lib/repair-script.js +266 -0
- package/lib/scripts.js +994 -0
- package/lib/seal.mjs +464 -0
- package/lib/sealed-qualifier.mjs +211 -0
- package/lib/security.js +59 -0
- package/lib/segment.js +369 -0
- package/lib/seo.js +275 -0
- package/lib/serve.js +854 -0
- package/lib/string-classify.js +85 -0
- package/lib/submit.mjs +344 -0
- package/lib/sync.js +969 -0
- package/lib/tags/bcp47.js +202 -0
- package/lib/tags/resolve.js +314 -0
- package/lib/terminology.js +111 -0
- package/lib/tm-seed.js +294 -0
- package/lib/tm.js +515 -0
- package/lib/translate-pair.js +197 -0
- package/lib/translate.js +203 -0
- package/lib/types.js +230 -0
- package/lib/validate.js +510 -0
- package/lib/verify.js +451 -0
- package/lib/watch.js +145 -0
- package/lib/xliff.js +184 -0
- package/package.json +93 -0
- package/shared/ATTRIBUTION.md +145 -0
- package/shared/CORPORA-CARDS.md +288 -0
- package/shared/DATA-SOVEREIGNTY.md +500 -0
- package/shared/LANGUAGE-CARD-FIELDS.md +532 -0
- package/shared/card-lint-baseline.json +3189 -0
- package/shared/cards-fallback.json +1 -0
- package/shared/catalogue/card-config.json +6091 -0
- package/shared/catalogue/external-results.json +3888 -0
- package/shared/catalogue/gender-guidance.json +1038 -0
- package/shared/catalogue/method-coverage.json +1751 -0
- package/shared/catalogue/metric-coverage.json +170 -0
- package/shared/catalogue/metric-reliability.json +1 -0
- package/shared/catalogue/register-presets.json +3180 -0
- package/shared/catalogue/vitality-scales.json +55 -0
- package/shared/cldr-index.json +1115 -0
- package/shared/code-bridge.json +253 -0
- package/shared/corpora-cards-v1-reference.md +281 -0
- package/shared/curated-dictionary-flags.json +35 -0
- package/shared/curated-endonyms.json +35 -0
- package/shared/curated-fsts.json +51 -0
- package/shared/curated-orthography-conventions.json +26 -0
- package/shared/curated-sil-resources.json +374 -0
- package/shared/curated-tools.json +41 -0
- package/shared/docent/corpus.json +11333 -0
- package/shared/docent/faq.en.json +564 -0
- package/shared/docent/register-blocks.json +60 -0
- package/shared/docent/system-prompt.md +144 -0
- package/shared/domain-taxonomy.json +35 -0
- package/shared/explainers/glossary.json +2975 -0
- package/shared/explainers/tc-features.json +20112 -0
- package/shared/explainers/term-watchlist.json +147 -0
- package/shared/human-services.json +59 -0
- package/shared/license-corrections.json +261 -0
- package/shared/license-evidence.json +13452 -0
- package/shared/licenses.json +6781 -0
- package/shared/method-registry.json +236 -0
- package/shared/metric-registry.json +620 -0
- package/shared/model-aliases.json +7 -0
- package/shared/schemas/champollion-plugin.schema.json +206 -0
- package/shared/schemas/corpora-card.schema.json +957 -0
- package/shared/schemas/domain-taxonomy.schema.json +64 -0
- package/shared/schemas/external-results.schema.json +314 -0
- package/shared/schemas/human-services.schema.json +90 -0
- package/shared/schemas/language-card.schema.json +1308 -0
- package/shared/schemas/licenses.schema.json +155 -0
- package/shared/schemas/method-card.schema.json +412 -0
- package/shared/schemas/method-registry.schema.json +85 -0
- package/shared/schemas/metric-registry.schema.json +96 -0
- package/shared/schemas/metric-reliability.schema.json +178 -0
- package/shared/schemas/model-aliases.schema.json +27 -0
- package/shared/schemas/source-snapshot.schema.json +96 -0
|
@@ -0,0 +1,1259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: init
|
|
3
|
+
*
|
|
4
|
+
* Interactive setup wizard that creates a v3 config file.
|
|
5
|
+
* Uses Node.js built-in readline — zero external dependencies.
|
|
6
|
+
*
|
|
7
|
+
* When run non-interactively (piped stdin, CI, or --yes flag), generates
|
|
8
|
+
* a default config from flags (--langs, --method, --model, ...) instead.
|
|
9
|
+
* In interactive mode the same flags prefill the wizard's defaults, so
|
|
10
|
+
* `champollion init --langs fr,de` walks the wizard with fr,de ready to
|
|
11
|
+
* accept. Flag values are validated up front — a typo'd --method or an
|
|
12
|
+
* out-of-range --temperature fails loudly before anything is written.
|
|
13
|
+
*
|
|
14
|
+
* Wizard flow (6 steps):
|
|
15
|
+
* 1. Languages — source locale + target languages (with presets)
|
|
16
|
+
* 2. Registers — guided tone/formality selection per language
|
|
17
|
+
* 3. Translation Method — accept defaults, pick one, or configure per language
|
|
18
|
+
* 4. Temperature — sampling temperature for LLM determinism control
|
|
19
|
+
* 5. Content Translation — Hugo, Docusaurus, or none
|
|
20
|
+
* 6. Confirm — review summary, write config, show next steps
|
|
21
|
+
*
|
|
22
|
+
* WHY registers before method: After choosing languages, the user should
|
|
23
|
+
* immediately learn about each language's formality system. This is the core
|
|
24
|
+
* value proposition. Method selection then follows — the user can make an
|
|
25
|
+
* informed choice knowing that LLM methods respect register prompts while
|
|
26
|
+
* API methods (except DeepL) ignore them.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import readline from 'node:readline';
|
|
32
|
+
import { CONFIG_FILENAMES, DEFAULT_OPENROUTER_MODEL, DEFAULT_BATCH_SIZE, DEFAULT_TEMPERATURE, DEFAULT_COACHED_TEMPERATURE, detectDocusaurus } from '../config.js';
|
|
33
|
+
import { DEFAULT_REGISTERS, getLanguageCard, getRegisterPresets, getMethodSupport } from '../registers.js';
|
|
34
|
+
import { getConverterInfo, resolveTargetScript, converterKeyForLocale } from '../scripts.js';
|
|
35
|
+
import { fetchAvailableModels, resolveProviderApiKey, isListableProvider, getProviderLabel } from '../models.js';
|
|
36
|
+
import { showCommandHelp } from '../command-help.js';
|
|
37
|
+
import { output } from '../output.js';
|
|
38
|
+
|
|
39
|
+
const DEFAULT_CONFIG_FILENAME = CONFIG_FILENAMES[0]; // champollion.config.json
|
|
40
|
+
|
|
41
|
+
// Popular language groups for quick selection
|
|
42
|
+
const LANGUAGE_PRESETS = {
|
|
43
|
+
european: ['fr', 'de', 'es', 'it', 'pt', 'nl'],
|
|
44
|
+
asian: ['ja', 'zh', 'ko'],
|
|
45
|
+
global: ['fr', 'es', 'de', 'ja', 'zh', 'ko', 'pt', 'ar'],
|
|
46
|
+
nordic: ['da', 'fi', 'nb', 'sv'],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Available translation methods — presented in the wizard as numbered options.
|
|
51
|
+
* Order matters: OpenRouter first (default), then GT, then direct LLM APIs,
|
|
52
|
+
* then other API services.
|
|
53
|
+
*
|
|
54
|
+
* Each entry contains the method name (matching METHOD_REGISTRY in translate.js),
|
|
55
|
+
* a display label, a short description, the env var needed, and whether it's
|
|
56
|
+
* an LLM method (which means we should ask about the model).
|
|
57
|
+
*/
|
|
58
|
+
const METHOD_OPTIONS = [
|
|
59
|
+
{
|
|
60
|
+
method: 'llm',
|
|
61
|
+
label: 'OpenRouter',
|
|
62
|
+
desc: '200+ models via one API. Most flexible.',
|
|
63
|
+
envVar: 'OPENROUTER_API_KEY',
|
|
64
|
+
isLLM: true,
|
|
65
|
+
category: 'llm',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
method: 'openai',
|
|
69
|
+
label: 'OpenAI (GPT-4o)',
|
|
70
|
+
desc: 'Direct OpenAI API. Strong for European languages.',
|
|
71
|
+
envVar: 'OPENAI_API_KEY',
|
|
72
|
+
isLLM: true,
|
|
73
|
+
category: 'llm',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
method: 'anthropic',
|
|
77
|
+
label: 'Anthropic (Claude)',
|
|
78
|
+
desc: 'Direct Anthropic API. Strong for nuanced text.',
|
|
79
|
+
envVar: 'ANTHROPIC_API_KEY',
|
|
80
|
+
isLLM: true,
|
|
81
|
+
category: 'llm',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
method: 'gemini',
|
|
85
|
+
label: 'Google Gemini',
|
|
86
|
+
desc: 'Free tier available. Good quality.',
|
|
87
|
+
envVar: 'GEMINI_API_KEY',
|
|
88
|
+
isLLM: true,
|
|
89
|
+
category: 'llm',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
method: 'deepl',
|
|
93
|
+
label: 'DeepL',
|
|
94
|
+
desc: 'Built-in formality for some languages. 30+ languages.',
|
|
95
|
+
envVar: 'DEEPL_API_KEY',
|
|
96
|
+
isLLM: false,
|
|
97
|
+
category: 'api',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
method: 'microsoft-translator',
|
|
101
|
+
label: 'Microsoft Translator',
|
|
102
|
+
desc: 'Azure Cognitive Services. 100+ languages.',
|
|
103
|
+
envVar: 'MICROSOFT_TRANSLATOR_API_KEY',
|
|
104
|
+
isLLM: false,
|
|
105
|
+
category: 'api',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
method: 'libretranslate',
|
|
109
|
+
label: 'LibreTranslate',
|
|
110
|
+
desc: 'Open source, self-hosted. Privacy-first.',
|
|
111
|
+
envVar: 'LIBRETRANSLATE_API_URL',
|
|
112
|
+
isLLM: false,
|
|
113
|
+
category: 'api',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
method: 'google-translate',
|
|
117
|
+
label: 'Google Translate',
|
|
118
|
+
desc: 'Cheapest at scale. 130+ languages.',
|
|
119
|
+
envVar: 'GOOGLE_TRANSLATE_API_KEY',
|
|
120
|
+
isLLM: false,
|
|
121
|
+
category: 'api',
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Checks whether stdin is interactive (attached to a TTY).
|
|
127
|
+
* If piped or in CI, we skip the interactive wizard.
|
|
128
|
+
*/
|
|
129
|
+
function isInteractive() {
|
|
130
|
+
return process.stdin.isTTY === true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Prompt the user for a single line of input.
|
|
135
|
+
* Returns the trimmed response, or the default if empty.
|
|
136
|
+
*/
|
|
137
|
+
function ask(rl, question, defaultValue) {
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
const suffix = defaultValue ? ` (${defaultValue})` : '';
|
|
140
|
+
rl.question(` ${question}${suffix}: `, (answer) => {
|
|
141
|
+
resolve(answer.trim() || defaultValue || '');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Parse a comma-separated language input string.
|
|
148
|
+
* Supports preset names (e.g., "european") and individual codes.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} input - Raw user input
|
|
151
|
+
* @returns {string[]} Deduplicated array of locale codes
|
|
152
|
+
*/
|
|
153
|
+
function parseLanguageInput(input) {
|
|
154
|
+
if (!input) return [];
|
|
155
|
+
|
|
156
|
+
const codes = new Set();
|
|
157
|
+
const parts = input.split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
|
|
158
|
+
|
|
159
|
+
for (const part of parts) {
|
|
160
|
+
if (LANGUAGE_PRESETS[part]) {
|
|
161
|
+
// Expand preset into individual codes
|
|
162
|
+
for (const code of LANGUAGE_PRESETS[part]) {
|
|
163
|
+
codes.add(code);
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
codes.add(part);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return [...codes];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get the METHOD_OPTIONS entry by its 1-based index or method name.
|
|
175
|
+
* Returns null if not found.
|
|
176
|
+
*/
|
|
177
|
+
function getMethodOption(input) {
|
|
178
|
+
const num = parseInt(input, 10);
|
|
179
|
+
if (num >= 1 && num <= METHOD_OPTIONS.length) {
|
|
180
|
+
return METHOD_OPTIONS[num - 1];
|
|
181
|
+
}
|
|
182
|
+
// Also accept method name directly (e.g., "google-translate")
|
|
183
|
+
return METHOD_OPTIONS.find(m => m.method === input) || null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Collect the set of unique env vars needed for the chosen methods.
|
|
188
|
+
* Used in the "Next steps" output.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} defaultMethod - The default method name
|
|
191
|
+
* @param {object|null} languageOverrides - languages object with per-lang method overrides
|
|
192
|
+
* @returns {Array<{envVar: string, label: string}>} Unique env vars needed
|
|
193
|
+
*/
|
|
194
|
+
function collectRequiredEnvVars(defaultMethod, languageOverrides) {
|
|
195
|
+
const needed = new Map();
|
|
196
|
+
|
|
197
|
+
// Add the default method's env var
|
|
198
|
+
const defaultOpt = METHOD_OPTIONS.find(m => m.method === defaultMethod);
|
|
199
|
+
if (defaultOpt) {
|
|
200
|
+
needed.set(defaultOpt.envVar, defaultOpt.label);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Add per-language method env vars
|
|
204
|
+
if (languageOverrides && typeof languageOverrides === 'object') {
|
|
205
|
+
for (const langConfig of Object.values(languageOverrides)) {
|
|
206
|
+
if (typeof langConfig === 'object' && langConfig.method) {
|
|
207
|
+
const opt = METHOD_OPTIONS.find(m => m.method === langConfig.method);
|
|
208
|
+
if (opt && !needed.has(opt.envVar)) {
|
|
209
|
+
needed.set(opt.envVar, opt.label);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return [...needed.entries()].map(([envVar, label]) => ({ envVar, label }));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Wizard Steps ──────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Step 1: Languages — source locale and target languages.
|
|
222
|
+
*/
|
|
223
|
+
async function stepLanguages(rl, prefill = {}) {
|
|
224
|
+
console.log('');
|
|
225
|
+
console.log(' Step 1/6 — Languages');
|
|
226
|
+
console.log(' ────────────────────────────────────────────────');
|
|
227
|
+
console.log('');
|
|
228
|
+
|
|
229
|
+
const source = await ask(rl, 'Source locale', prefill.source || 'en');
|
|
230
|
+
|
|
231
|
+
console.log('');
|
|
232
|
+
console.log(' Target languages — enter codes separated by commas.');
|
|
233
|
+
console.log(' Presets: european (fr,de,es,it,pt,nl) | asian (ja,zh,ko)');
|
|
234
|
+
console.log(' global (fr,es,de,ja,zh,ko,pt,ar) | nordic (da,fi,nb,sv)');
|
|
235
|
+
console.log(' Example: fr, de, ja or european, ja');
|
|
236
|
+
console.log(' Leave blank to auto-detect from your locales directory.');
|
|
237
|
+
const langInput = await ask(rl, 'Target languages', prefill.langs || '');
|
|
238
|
+
const languages = parseLanguageInput(langInput);
|
|
239
|
+
const scriptChoices = {};
|
|
240
|
+
|
|
241
|
+
if (languages.length > 0) {
|
|
242
|
+
console.log('');
|
|
243
|
+
console.log(' Selected:');
|
|
244
|
+
for (const code of languages) {
|
|
245
|
+
const card = getLanguageCard(code);
|
|
246
|
+
const name = card ? card.name : (DEFAULT_REGISTERS[code]?.name || code);
|
|
247
|
+
const system = card?.formality?.system;
|
|
248
|
+
const systemLabel = system ? ` (${system})` : '';
|
|
249
|
+
const unknown = !card && !DEFAULT_REGISTERS[code] ? ' ⚠ unrecognized code — check spelling' : '';
|
|
250
|
+
console.log(` ${code} — ${name}${systemLabel}${unknown}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Script decisions, made HERE — at language selection, while the user is
|
|
254
|
+
// looking at the language — never defaulted later:
|
|
255
|
+
// - A locale with two real orthographies (crk: SRO/Syllabics, sr:
|
|
256
|
+
// Latin/Cyrillic) REQUIRES a choice. Champollion won't pick a
|
|
257
|
+
// community's writing system; sync refuses to run until one is set.
|
|
258
|
+
// - A locale whose display script is Private Use Area (tlh pIqaD,
|
|
259
|
+
// Tengwar, Kryptonian — not in Unicode) defaults to romanization,
|
|
260
|
+
// the only output that renders without a custom font; opting in is
|
|
261
|
+
// explained, not assumed.
|
|
262
|
+
for (const code of languages) {
|
|
263
|
+
const card = getLanguageCard(code);
|
|
264
|
+
let resolution;
|
|
265
|
+
try {
|
|
266
|
+
resolution = resolveTargetScript(code, {}, card);
|
|
267
|
+
} catch {
|
|
268
|
+
continue; // unrecognized code — already flagged above
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (resolution.source === 'choice-required') {
|
|
272
|
+
console.log('');
|
|
273
|
+
console.log(` ${code} is written in more than one orthography:`);
|
|
274
|
+
resolution.choices.forEach((c, i) => {
|
|
275
|
+
console.log(` ${i + 1}. ${c.label} ("script": "${c.script}")`);
|
|
276
|
+
});
|
|
277
|
+
const pick = await ask(rl, `Which should Champollion write for ${code}?`, '1');
|
|
278
|
+
const idx = parseInt(pick, 10) - 1;
|
|
279
|
+
const chosen = resolution.choices[idx] || resolution.choices[0];
|
|
280
|
+
scriptChoices[code] = chosen.script;
|
|
281
|
+
console.log(` → ${code}: "script": "${chosen.script}" (${chosen.label})`);
|
|
282
|
+
} else if (resolution.source === 'default') {
|
|
283
|
+
const info = getConverterInfo(converterKeyForLocale(code, card));
|
|
284
|
+
console.log('');
|
|
285
|
+
console.log(` ℹ ${code} will be written in ${info.from}.`);
|
|
286
|
+
console.log(` Its display script (${info.to}) is not in Unicode and needs a special font.`);
|
|
287
|
+
console.log(` To emit it instead, set "script": "${info.toScript || converterKeyForLocale(code, card)}" for ${code}`);
|
|
288
|
+
console.log(' and run `champollion fonts install`.');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { source, languages, scriptChoices };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Step 3: Translation method — accept defaults, pick one, or per-language.
|
|
298
|
+
*
|
|
299
|
+
* Returns:
|
|
300
|
+
* { defaultMethod, defaultModel, perLanguage: null } — options 1 or 2
|
|
301
|
+
* { defaultMethod, defaultModel, perLanguage: { ... } } — option 3
|
|
302
|
+
*/
|
|
303
|
+
async function stepMethod(rl, languages, presetModel = null) {
|
|
304
|
+
const defaultModel = presetModel || DEFAULT_OPENROUTER_MODEL;
|
|
305
|
+
|
|
306
|
+
console.log('');
|
|
307
|
+
console.log(' Step 3/6 — Translation Method');
|
|
308
|
+
console.log(' ────────────────────────────────────────────────');
|
|
309
|
+
console.log('');
|
|
310
|
+
console.log(` Default: OpenRouter → ${defaultModel}`);
|
|
311
|
+
console.log('');
|
|
312
|
+
console.log(' 1. Accept defaults');
|
|
313
|
+
console.log(' 2. Choose a different method for all languages');
|
|
314
|
+
console.log(' 3. Configure each language individually');
|
|
315
|
+
console.log('');
|
|
316
|
+
|
|
317
|
+
const choice = await ask(rl, 'Choose', '1');
|
|
318
|
+
|
|
319
|
+
// ── Option 1: Accept defaults ──
|
|
320
|
+
if (choice === '1') {
|
|
321
|
+
return {
|
|
322
|
+
defaultMethod: 'llm',
|
|
323
|
+
defaultModel,
|
|
324
|
+
perLanguage: null,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Option 2: Single method for all ──
|
|
329
|
+
if (choice === '2') {
|
|
330
|
+
return await pickSingleMethod(rl, presetModel);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ── Option 3: Per-language configuration ──
|
|
334
|
+
if (choice === '3') {
|
|
335
|
+
return await pickPerLanguageMethod(rl, languages, presetModel);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Default fallback
|
|
339
|
+
console.log(` Unrecognized choice "${choice}" — accepting defaults.`);
|
|
340
|
+
return {
|
|
341
|
+
defaultMethod: 'llm',
|
|
342
|
+
defaultModel,
|
|
343
|
+
perLanguage: null,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Show the method picker with categorized display and guidance.
|
|
349
|
+
*
|
|
350
|
+
* LLM methods are shown first (register-aware, best quality), then
|
|
351
|
+
* API methods (fast/cheap, no register control except DeepL).
|
|
352
|
+
*/
|
|
353
|
+
async function pickSingleMethod(rl, presetModel = null) {
|
|
354
|
+
const llmMethods = METHOD_OPTIONS.filter(m => m.category === 'llm');
|
|
355
|
+
const apiMethods = METHOD_OPTIONS.filter(m => m.category === 'api');
|
|
356
|
+
|
|
357
|
+
console.log('');
|
|
358
|
+
console.log(' LLM methods — context-aware, uses your register presets:');
|
|
359
|
+
console.log('');
|
|
360
|
+
let idx = 1;
|
|
361
|
+
for (const m of llmMethods) {
|
|
362
|
+
const num = String(idx).padStart(4, ' ');
|
|
363
|
+
console.log(` ${num}. ${m.label.padEnd(24)} ${m.desc}`);
|
|
364
|
+
idx++;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
console.log('');
|
|
368
|
+
console.log(' API methods — fast and affordable, no register control:');
|
|
369
|
+
console.log('');
|
|
370
|
+
for (const m of apiMethods) {
|
|
371
|
+
const num = String(idx).padStart(4, ' ');
|
|
372
|
+
console.log(` ${num}. ${m.label.padEnd(24)} ${m.desc}`);
|
|
373
|
+
idx++;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
console.log('');
|
|
377
|
+
console.log(' Tip: LLM methods use your register presets to control tone.');
|
|
378
|
+
console.log(' API methods translate without tone guidance (except DeepL).');
|
|
379
|
+
console.log('');
|
|
380
|
+
|
|
381
|
+
const methodChoice = await ask(rl, 'Choose', '1');
|
|
382
|
+
const num = parseInt(methodChoice, 10);
|
|
383
|
+
const selected = (num >= 1 && num <= METHOD_OPTIONS.length)
|
|
384
|
+
? METHOD_OPTIONS[num - 1]
|
|
385
|
+
: METHOD_OPTIONS.find(m => m.method === methodChoice) || null;
|
|
386
|
+
|
|
387
|
+
if (!selected) {
|
|
388
|
+
console.log(' Invalid choice — using default (OpenRouter).');
|
|
389
|
+
return { defaultMethod: 'llm', defaultModel: DEFAULT_OPENROUTER_MODEL, perLanguage: null };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Resolve model — the core of the provider-first workflow
|
|
393
|
+
let model = null;
|
|
394
|
+
if (selected.isLLM) {
|
|
395
|
+
model = await pickModelForProvider(rl, selected.method, presetModel);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Warn if an API method was chosen — registers won't have full effect
|
|
399
|
+
if (selected.category === 'api' && selected.method !== 'deepl') {
|
|
400
|
+
console.log('');
|
|
401
|
+
console.log(` Note: ${selected.label} doesn't use register presets — your formality`);
|
|
402
|
+
console.log(' choices from Step 2 will only apply if you switch to an LLM method later.');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
console.log(` → ${selected.label}${model ? ' / ' + model : ''}`);
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
defaultMethod: selected.method,
|
|
409
|
+
defaultModel: model,
|
|
410
|
+
perLanguage: null,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Pick a model for a direct LLM provider via the provider-first workflow.
|
|
416
|
+
*
|
|
417
|
+
* Flow:
|
|
418
|
+
* 1. Check if the provider's API key is in the environment
|
|
419
|
+
* 2. If found: fetch real model list, show numbered picker
|
|
420
|
+
* 3. If not found: warn, return null (method uses its own default at runtime)
|
|
421
|
+
* 4. If fetch fails: fallback to manual text input
|
|
422
|
+
*
|
|
423
|
+
* For OpenRouter (method='llm'): skips the dynamic picker because OpenRouter
|
|
424
|
+
* has 200+ models with no popularity ranking. Users type their preferred slug.
|
|
425
|
+
*
|
|
426
|
+
* @param {object} rl - Readline interface
|
|
427
|
+
* @param {string} method - Provider method name (e.g., 'gemini', 'openai')
|
|
428
|
+
* @returns {Promise<string|null>} Selected model ID, or null
|
|
429
|
+
*/
|
|
430
|
+
async function pickModelForProvider(rl, method, presetModel = null) {
|
|
431
|
+
// OpenRouter: too many models for a picker, user types their slug
|
|
432
|
+
if (method === 'llm') {
|
|
433
|
+
return await ask(rl, 'Model', presetModel || DEFAULT_OPENROUTER_MODEL);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Direct providers: try to fetch the real model list
|
|
437
|
+
if (!isListableProvider(method)) {
|
|
438
|
+
// Not a provider we can query — manual input
|
|
439
|
+
return await ask(rl, 'Model', presetModel || '');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const apiKey = resolveProviderApiKey(method);
|
|
443
|
+
if (!apiKey) {
|
|
444
|
+
const label = getProviderLabel(method);
|
|
445
|
+
console.log('');
|
|
446
|
+
console.log(` ⚠ ${label} API key not found in environment.`);
|
|
447
|
+
console.log(` Set it, then run \`champollion models --method ${method}\` to see available models.`);
|
|
448
|
+
console.log(' The method will use its built-in default model at runtime.');
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Fetch real models from the provider API
|
|
453
|
+
const label = getProviderLabel(method);
|
|
454
|
+
console.log(`\n Fetching models from ${label}...`);
|
|
455
|
+
const models = await fetchAvailableModels(method, apiKey);
|
|
456
|
+
|
|
457
|
+
if (!models || models.length === 0) {
|
|
458
|
+
console.log(' Could not fetch model list. Enter a model ID manually:');
|
|
459
|
+
return await ask(rl, 'Model', presetModel || '') || null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Display as a numbered picker — provider sort order (recency/capability)
|
|
463
|
+
console.log('');
|
|
464
|
+
const displayCount = Math.min(models.length, 20); // cap the list for readability
|
|
465
|
+
for (let i = 0; i < displayCount; i++) {
|
|
466
|
+
console.log(` ${String(i + 1).padStart(3)}. ${models[i]}`);
|
|
467
|
+
}
|
|
468
|
+
if (models.length > displayCount) {
|
|
469
|
+
console.log(` ... and ${models.length - displayCount} more (run \`champollion models --method ${method}\` to see all)`);
|
|
470
|
+
}
|
|
471
|
+
console.log('');
|
|
472
|
+
|
|
473
|
+
const modelChoice = await ask(rl, 'Choose (number or model ID)', '1');
|
|
474
|
+
const modelNum = parseInt(modelChoice, 10);
|
|
475
|
+
|
|
476
|
+
if (modelNum >= 1 && modelNum <= models.length) {
|
|
477
|
+
return models[modelNum - 1];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// User typed a model slug directly — use as-is
|
|
481
|
+
if (modelChoice.trim()) {
|
|
482
|
+
return modelChoice.trim();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Empty input — use the first model from the list
|
|
486
|
+
return models[0];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Walk through each target language and let the user pick a method.
|
|
491
|
+
*/
|
|
492
|
+
async function pickPerLanguageMethod(rl, languages, presetModel = null) {
|
|
493
|
+
const fallbackModel = presetModel || DEFAULT_OPENROUTER_MODEL;
|
|
494
|
+
|
|
495
|
+
if (languages.length === 0) {
|
|
496
|
+
console.log(' No target languages specified — using defaults.');
|
|
497
|
+
return { defaultMethod: 'llm', defaultModel: fallbackModel, perLanguage: null };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
console.log('');
|
|
501
|
+
console.log(' Configure method for each language:');
|
|
502
|
+
console.log(` (Enter a number 1-${METHOD_OPTIONS.length}, or press Enter for default)`);
|
|
503
|
+
console.log('');
|
|
504
|
+
|
|
505
|
+
// Show a compact method reference
|
|
506
|
+
for (let i = 0; i < METHOD_OPTIONS.length; i++) {
|
|
507
|
+
const m = METHOD_OPTIONS[i];
|
|
508
|
+
console.log(` ${i + 1}. ${m.label}`);
|
|
509
|
+
}
|
|
510
|
+
console.log('');
|
|
511
|
+
|
|
512
|
+
const perLanguage = {};
|
|
513
|
+
|
|
514
|
+
for (const code of languages) {
|
|
515
|
+
const card = getLanguageCard(code);
|
|
516
|
+
const name = card?.name || DEFAULT_REGISTERS[code]?.name || code;
|
|
517
|
+
|
|
518
|
+
const methodChoice = await ask(rl, ` ${code} (${name}) — method`, '1');
|
|
519
|
+
const selected = getMethodOption(methodChoice);
|
|
520
|
+
|
|
521
|
+
if (!selected || selected.method === 'llm') {
|
|
522
|
+
// Default — no per-language override needed
|
|
523
|
+
console.log(` → OpenRouter / ${fallbackModel}`);
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Check if this method supports the language via card metadata.
|
|
528
|
+
// WHY: A user picking DeepL for Swahili should know it's not supported.
|
|
529
|
+
const support = getMethodSupport(code);
|
|
530
|
+
if (support) {
|
|
531
|
+
const methodKey = selected.method === 'google-translate' ? 'googleTranslate'
|
|
532
|
+
: selected.method === 'microsoft-translator' ? 'microsoftTranslator'
|
|
533
|
+
: selected.method;
|
|
534
|
+
if (support[methodKey] === false) {
|
|
535
|
+
console.log(` ⚠ ${selected.label} may not support ${name}. Consider LLM instead.`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Build per-language config entry
|
|
540
|
+
const langEntry = { method: selected.method };
|
|
541
|
+
|
|
542
|
+
if (selected.isLLM) {
|
|
543
|
+
// Use the same provider-first model picker for per-language selection
|
|
544
|
+
const model = await pickModelForProvider(rl, selected.method, presetModel);
|
|
545
|
+
if (model) {
|
|
546
|
+
langEntry.model = model;
|
|
547
|
+
}
|
|
548
|
+
console.log(` → ${selected.label}${model ? ' / ' + model : ''}`);
|
|
549
|
+
} else {
|
|
550
|
+
console.log(` → ${selected.label}`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
perLanguage[code] = langEntry;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
defaultMethod: 'llm',
|
|
558
|
+
defaultModel: fallbackModel,
|
|
559
|
+
perLanguage: Object.keys(perLanguage).length > 0 ? perLanguage : null,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Step 2: Registers — guided tone/formality selection per language.
|
|
565
|
+
*
|
|
566
|
+
* Compact view: shows each language's formality system and default preset.
|
|
567
|
+
* Expand-on-demand: type a language code to see all available presets
|
|
568
|
+
* with descriptions and pick one.
|
|
569
|
+
*
|
|
570
|
+
* Returns an object mapping language codes to preset keys (explicit, even
|
|
571
|
+
* for defaults) so the config file is self-documenting.
|
|
572
|
+
*
|
|
573
|
+
* WHY proactive display: Registers are the core value proposition —
|
|
574
|
+
* the init wizard should SHOW the user what formality systems exist
|
|
575
|
+
* and explain the defaults, not hide them behind a y/N gate.
|
|
576
|
+
*
|
|
577
|
+
* @param {object} rl - Readline interface
|
|
578
|
+
* @param {string[]} languages - Target language codes
|
|
579
|
+
* @returns {object|null} Map of code → preset key, or null if no languages
|
|
580
|
+
*/
|
|
581
|
+
async function stepRegisters(rl, languages) {
|
|
582
|
+
if (languages.length === 0) return null;
|
|
583
|
+
|
|
584
|
+
console.log('');
|
|
585
|
+
console.log(' Step 2/6 — Registers');
|
|
586
|
+
console.log(' ────────────────────────────────────────────────');
|
|
587
|
+
console.log('');
|
|
588
|
+
console.log(' Registers tell the translator how your app should sound.');
|
|
589
|
+
console.log(' Each language has a formality system — champollion picks a');
|
|
590
|
+
console.log(' sensible default for software UI.');
|
|
591
|
+
console.log('');
|
|
592
|
+
|
|
593
|
+
// Build the per-language summary and track default preset keys.
|
|
594
|
+
// selectedPresets stores the explicitly chosen (or default) key for each language.
|
|
595
|
+
const selectedPresets = {};
|
|
596
|
+
|
|
597
|
+
for (const code of languages) {
|
|
598
|
+
const card = getLanguageCard(code);
|
|
599
|
+
const presets = getRegisterPresets(code);
|
|
600
|
+
const name = card?.name || DEFAULT_REGISTERS[code]?.name || code;
|
|
601
|
+
const system = card?.formality?.system || null;
|
|
602
|
+
const systemLabel = system ? ` [${system}]` : '';
|
|
603
|
+
|
|
604
|
+
if (presets.length > 0) {
|
|
605
|
+
const defaultPreset = presets.find(p => p.isDefault) || presets[0];
|
|
606
|
+
// Explicitly store the default preset key — makes config self-documenting
|
|
607
|
+
selectedPresets[code] = defaultPreset.key;
|
|
608
|
+
|
|
609
|
+
// Compact display: code, name, system, default preset label
|
|
610
|
+
const altCount = presets.length - 1;
|
|
611
|
+
const altText = altCount > 0 ? ` (${altCount} alternative${altCount > 1 ? 's' : ''})` : '';
|
|
612
|
+
console.log(` ${code.padEnd(6)} ${name}${systemLabel}`);
|
|
613
|
+
console.log(` → ${defaultPreset.label} ★${altText}`);
|
|
614
|
+
} else {
|
|
615
|
+
// No card / no presets — use generic fallback
|
|
616
|
+
selectedPresets[code] = null;
|
|
617
|
+
console.log(` ${code.padEnd(6)} ${name}`);
|
|
618
|
+
console.log(` → Professional register (default)`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
console.log('');
|
|
623
|
+
console.log(' ────────────────────────────────────────────────');
|
|
624
|
+
console.log(' Press Enter to accept all defaults (★).');
|
|
625
|
+
console.log(' Type a language code to see options and change it.');
|
|
626
|
+
|
|
627
|
+
// Interactive loop: user can adjust one language at a time, or Enter to finish
|
|
628
|
+
let adjusting = true;
|
|
629
|
+
while (adjusting) {
|
|
630
|
+
console.log('');
|
|
631
|
+
const input = await ask(rl, 'Adjust a language (or Enter to continue)', '');
|
|
632
|
+
|
|
633
|
+
if (!input) {
|
|
634
|
+
adjusting = false;
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Find the matching language code
|
|
639
|
+
const code = input.toLowerCase();
|
|
640
|
+
if (!languages.includes(code)) {
|
|
641
|
+
console.log(` "${code}" is not in your target languages.`);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const presets = getRegisterPresets(code);
|
|
646
|
+
const card = getLanguageCard(code);
|
|
647
|
+
const name = card?.name || DEFAULT_REGISTERS[code]?.name || code;
|
|
648
|
+
|
|
649
|
+
if (presets.length === 0) {
|
|
650
|
+
// No presets — offer custom text only
|
|
651
|
+
const custom = await ask(rl, ` ${name} — custom register text`, '');
|
|
652
|
+
if (custom) {
|
|
653
|
+
selectedPresets[code] = custom;
|
|
654
|
+
console.log(` → Custom: "${custom.substring(0, 50)}${custom.length > 50 ? '...' : ''}"`);
|
|
655
|
+
}
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Expanded view: show all presets with descriptions
|
|
660
|
+
console.log('');
|
|
661
|
+
console.log(` ${code} — ${name}${card?.formality?.system ? ` [${card.formality.system}]` : ''}`);
|
|
662
|
+
|
|
663
|
+
// Show the formality system description from the card — this is the
|
|
664
|
+
// "why this matters" context that makes the wizard prescriptive.
|
|
665
|
+
if (card?.formality?.description) {
|
|
666
|
+
// Wrap the description to ~64 chars for terminal readability
|
|
667
|
+
const desc = card.formality.description;
|
|
668
|
+
const words = desc.split(' ');
|
|
669
|
+
let line = ' ';
|
|
670
|
+
for (const word of words) {
|
|
671
|
+
if (line.length + word.length > 68 && line.length > 4) {
|
|
672
|
+
console.log(line);
|
|
673
|
+
line = ' ' + word;
|
|
674
|
+
} else {
|
|
675
|
+
line += (line.length > 2 ? ' ' : '') + word;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (line.length > 2) console.log(line);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
console.log('');
|
|
682
|
+
for (let i = 0; i < presets.length; i++) {
|
|
683
|
+
const p = presets[i];
|
|
684
|
+
const marker = p.isDefault ? ' ★' : ' ';
|
|
685
|
+
const current = selectedPresets[code] === p.key ? ' (current)' : '';
|
|
686
|
+
console.log(` ${i + 1}.${marker} ${p.label}`);
|
|
687
|
+
console.log(` ${p.description}${current}`);
|
|
688
|
+
}
|
|
689
|
+
console.log(` c. Custom text`);
|
|
690
|
+
console.log('');
|
|
691
|
+
|
|
692
|
+
const defaultIdx = presets.findIndex(p => p.key === selectedPresets[code]) + 1 || 1;
|
|
693
|
+
const choice = await ask(rl, ` Choose`, String(defaultIdx));
|
|
694
|
+
|
|
695
|
+
if (choice.toLowerCase() === 'c') {
|
|
696
|
+
const custom = await ask(rl, ` Custom register text`, '');
|
|
697
|
+
if (custom) {
|
|
698
|
+
selectedPresets[code] = custom;
|
|
699
|
+
console.log(` → Custom register set.`);
|
|
700
|
+
}
|
|
701
|
+
} else {
|
|
702
|
+
const num = parseInt(choice, 10);
|
|
703
|
+
if (num >= 1 && num <= presets.length) {
|
|
704
|
+
const selected = presets[num - 1];
|
|
705
|
+
selectedPresets[code] = selected.key;
|
|
706
|
+
console.log(` → ${selected.label}`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return Object.keys(selectedPresets).length > 0 ? selectedPresets : null;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Step 4: Temperature — sampling temperature for LLM determinism control.
|
|
716
|
+
*
|
|
717
|
+
* Explains temperature in plain language and lets the user accept the default
|
|
718
|
+
* or enter a custom value. Returns null if default is accepted (so config
|
|
719
|
+
* only includes temperature when explicitly set).
|
|
720
|
+
*
|
|
721
|
+
* @param {readline.Interface} rl - Readline interface
|
|
722
|
+
* @param {string} defaultMethod - Selected method name (e.g., 'llm', 'llm-coached', 'deepl')
|
|
723
|
+
* @param {string|number|null} [presetTemp] - --temperature flag value; becomes the prompt default
|
|
724
|
+
* @returns {number|null} Custom temperature, or null for default
|
|
725
|
+
*/
|
|
726
|
+
async function stepTemperature(rl, defaultMethod, presetTemp = null) {
|
|
727
|
+
// Temperature only applies to LLM methods — skip for API-only methods
|
|
728
|
+
const isLLM = !defaultMethod || ['llm', 'llm-coached', 'openai', 'anthropic', 'gemini'].includes(defaultMethod);
|
|
729
|
+
if (!isLLM) return null;
|
|
730
|
+
|
|
731
|
+
const isCoached = defaultMethod === 'llm-coached';
|
|
732
|
+
const methodDefault = isCoached ? DEFAULT_COACHED_TEMPERATURE : DEFAULT_TEMPERATURE;
|
|
733
|
+
|
|
734
|
+
// --temperature prefill: a valid flag value becomes the prompt default,
|
|
735
|
+
// so Enter accepts it (run() validates flags, but guard anyway)
|
|
736
|
+
const preset = presetTemp != null ? parseFloat(presetTemp) : NaN;
|
|
737
|
+
const promptDefault = !isNaN(preset) && preset >= 0 && preset <= 1 ? preset : methodDefault;
|
|
738
|
+
|
|
739
|
+
console.log('');
|
|
740
|
+
console.log(' Step 4/6 — Temperature');
|
|
741
|
+
console.log(' ────────────────────────────────────────────────');
|
|
742
|
+
console.log('');
|
|
743
|
+
console.log(' Temperature controls how deterministic the translations are.');
|
|
744
|
+
console.log(' Lower values (0.1–0.3) produce more consistent, predictable output.');
|
|
745
|
+
console.log(' Higher values (0.5–0.8) allow more variation and creativity.');
|
|
746
|
+
console.log('');
|
|
747
|
+
console.log(' For software UI translation, lower is almost always better.');
|
|
748
|
+
if (isCoached) {
|
|
749
|
+
console.log(' The coached method uses 0.2 by default for extra consistency.');
|
|
750
|
+
}
|
|
751
|
+
console.log('');
|
|
752
|
+
|
|
753
|
+
const answer = await ask(rl, `Temperature (0.0–1.0)`, String(promptDefault));
|
|
754
|
+
|
|
755
|
+
const parsed = parseFloat(answer);
|
|
756
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 1) {
|
|
757
|
+
console.log(` Invalid temperature "${answer}" — using default ${methodDefault}.`);
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Return null if the user accepted the default (no need to clutter config)
|
|
762
|
+
if (parsed === methodDefault) return null;
|
|
763
|
+
|
|
764
|
+
return parsed;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Step 5: Content translation — Hugo, Docusaurus, or none.
|
|
769
|
+
*/
|
|
770
|
+
async function stepContent(rl, cwd) {
|
|
771
|
+
console.log('');
|
|
772
|
+
console.log(' Step 5/6 — Content Translation');
|
|
773
|
+
console.log(' ────────────────────────────────────────────────');
|
|
774
|
+
console.log('');
|
|
775
|
+
console.log(' Do you have Markdown content to translate?');
|
|
776
|
+
console.log('');
|
|
777
|
+
console.log(' 1. No — key-value locale files only');
|
|
778
|
+
console.log(' 2. Yes — Hugo content directory');
|
|
779
|
+
|
|
780
|
+
// Auto-detect Docusaurus
|
|
781
|
+
const hasDocusaurus = detectDocusaurus(cwd);
|
|
782
|
+
if (hasDocusaurus) {
|
|
783
|
+
console.log(' 3. Yes — Docusaurus (auto-detected ✓)');
|
|
784
|
+
} else {
|
|
785
|
+
console.log(' 3. Yes — Docusaurus');
|
|
786
|
+
}
|
|
787
|
+
console.log('');
|
|
788
|
+
|
|
789
|
+
// Docusaurus detected → make it the default answer; Enter accepts it
|
|
790
|
+
const choice = await ask(rl, 'Choose', hasDocusaurus ? '3' : '1');
|
|
791
|
+
|
|
792
|
+
if (choice === '2') {
|
|
793
|
+
const contentDir = await ask(rl, 'Hugo content directory', './content');
|
|
794
|
+
return { contentDir, format: null };
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (choice === '3') {
|
|
798
|
+
console.log(' → Docusaurus mode enabled. Locale files in ./i18n/');
|
|
799
|
+
return { contentDir: null, format: 'docusaurus' };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return { contentDir: null, format: null };
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Step 6: Confirm — show summary, write config, display next steps.
|
|
807
|
+
*/
|
|
808
|
+
async function stepConfirm(rl, config, envVars) {
|
|
809
|
+
console.log('');
|
|
810
|
+
console.log(' Step 6/6 — Confirm');
|
|
811
|
+
console.log(' ────────────────────────────────────────────────');
|
|
812
|
+
console.log('');
|
|
813
|
+
console.log(' Config Summary:');
|
|
814
|
+
console.log(' ──────────────────────────────────────');
|
|
815
|
+
|
|
816
|
+
console.log(` Source locale: ${config.inputLocale}`);
|
|
817
|
+
|
|
818
|
+
// Display languages — handle both array and object forms
|
|
819
|
+
if (Array.isArray(config.languages)) {
|
|
820
|
+
const display = config.languages.length > 0
|
|
821
|
+
? config.languages.join(', ')
|
|
822
|
+
: '(auto-detect from directory)';
|
|
823
|
+
console.log(` Target locales: ${display}`);
|
|
824
|
+
} else {
|
|
825
|
+
const codes = Object.keys(config.languages);
|
|
826
|
+
console.log(` Target locales: ${codes.join(', ')}`);
|
|
827
|
+
// Show per-language method overrides
|
|
828
|
+
for (const [code, langConfig] of Object.entries(config.languages)) {
|
|
829
|
+
if (typeof langConfig === 'object' && langConfig.method) {
|
|
830
|
+
const opt = METHOD_OPTIONS.find(m => m.method === langConfig.method);
|
|
831
|
+
const label = opt ? opt.label : langConfig.method;
|
|
832
|
+
const modelStr = langConfig.model ? ` / ${langConfig.model}` : '';
|
|
833
|
+
console.log(` ${code}: ${label}${modelStr}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
console.log(` Locales dir: ${config.localesDir}`);
|
|
839
|
+
console.log(` Format: ${config.format}`);
|
|
840
|
+
|
|
841
|
+
// Show default method if not the default 'llm'
|
|
842
|
+
if (config.defaultMethod && config.defaultMethod !== 'llm') {
|
|
843
|
+
const opt = METHOD_OPTIONS.find(m => m.method === config.defaultMethod);
|
|
844
|
+
console.log(` Method: ${opt ? opt.label : config.defaultMethod}`);
|
|
845
|
+
} else {
|
|
846
|
+
console.log(` Method: OpenRouter`);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (config.model) {
|
|
850
|
+
console.log(` Model: ${config.model}`);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (config.contentDir) {
|
|
854
|
+
console.log(` Content dir: ${config.contentDir}`);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
console.log(' ──────────────────────────────────────');
|
|
858
|
+
|
|
859
|
+
// Show required env vars
|
|
860
|
+
if (envVars.length > 0) {
|
|
861
|
+
console.log('');
|
|
862
|
+
console.log(' Required API key(s):');
|
|
863
|
+
for (const { envVar, label } of envVars) {
|
|
864
|
+
console.log(` ${envVar} (${label})`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
console.log('');
|
|
869
|
+
const confirm = await ask(rl, 'Write this config?', 'yes');
|
|
870
|
+
return confirm.toLowerCase().startsWith('y');
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Build the config object from wizard answers.
|
|
875
|
+
*
|
|
876
|
+
* The new register step stores explicit preset keys for every language
|
|
877
|
+
* (including defaults), so customRegisters is always populated when
|
|
878
|
+
* languages were selected. Per-language method overrides are merged in.
|
|
879
|
+
*
|
|
880
|
+
* Config languages format:
|
|
881
|
+
* - Object with preset keys: { "fr": "formal-vous", "ja": "polite" }
|
|
882
|
+
* - Object with full config: { "fr": { "register": "casual-tu", "method": "deepl" } }
|
|
883
|
+
* - Array: ["fr", "de", "ja"] — only when no registers or overrides
|
|
884
|
+
*/
|
|
885
|
+
function buildConfig(answers) {
|
|
886
|
+
const {
|
|
887
|
+
source, languages, defaultMethod, defaultModel,
|
|
888
|
+
perLanguage, customRegisters, temperature,
|
|
889
|
+
localesDir, format, contentDir, scriptChoices,
|
|
890
|
+
} = answers;
|
|
891
|
+
|
|
892
|
+
const hasPerLanguage = perLanguage && Object.keys(perLanguage).length > 0;
|
|
893
|
+
const hasRegisters = customRegisters && Object.keys(customRegisters).length > 0;
|
|
894
|
+
const hasScriptChoices = scriptChoices && Object.keys(scriptChoices).length > 0;
|
|
895
|
+
const needsObjectForm = hasPerLanguage || hasRegisters || hasScriptChoices;
|
|
896
|
+
|
|
897
|
+
let languagesConfig;
|
|
898
|
+
if (needsObjectForm) {
|
|
899
|
+
// Object form — stores preset keys and/or method overrides per language.
|
|
900
|
+
// When a language has both a register preset and a method override,
|
|
901
|
+
// it gets the full object form { register, method, model }.
|
|
902
|
+
// When it only has a register preset, it's stored as a bare string.
|
|
903
|
+
languagesConfig = {};
|
|
904
|
+
for (const code of languages) {
|
|
905
|
+
const methodOverride = perLanguage ? perLanguage[code] : null;
|
|
906
|
+
const registerValue = customRegisters ? customRegisters[code] : null;
|
|
907
|
+
// Orthography choice from the wizard (crk/sr-class dual-script
|
|
908
|
+
// locales). Persisted so sync never has to ask — or refuse — again.
|
|
909
|
+
const scriptValue = scriptChoices ? scriptChoices[code] : null;
|
|
910
|
+
|
|
911
|
+
if (methodOverride || scriptValue) {
|
|
912
|
+
// Needs full object form
|
|
913
|
+
const entry = {};
|
|
914
|
+
if (methodOverride?.method) entry.method = methodOverride.method;
|
|
915
|
+
if (methodOverride?.model) entry.model = methodOverride.model;
|
|
916
|
+
// Include register preset key if present
|
|
917
|
+
if (registerValue) entry.register = registerValue;
|
|
918
|
+
if (scriptValue) entry.script = scriptValue;
|
|
919
|
+
languagesConfig[code] = entry;
|
|
920
|
+
} else if (registerValue) {
|
|
921
|
+
// Register only — store as bare preset key string
|
|
922
|
+
languagesConfig[code] = registerValue;
|
|
923
|
+
} else {
|
|
924
|
+
// No overrides at all — store empty object to keep in object form
|
|
925
|
+
languagesConfig[code] = {};
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
} else {
|
|
929
|
+
// Simple array form — no registers, no overrides
|
|
930
|
+
languagesConfig = languages;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const config = {
|
|
934
|
+
version: 3,
|
|
935
|
+
inputLocale: source,
|
|
936
|
+
localesDir,
|
|
937
|
+
languages: languagesConfig,
|
|
938
|
+
batchSize: DEFAULT_BATCH_SIZE,
|
|
939
|
+
format,
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
// Only include model in config when the user explicitly chose one.
|
|
943
|
+
// null means the method class picks its own default at runtime—
|
|
944
|
+
// we don't want to write a stale hardcoded slug into the config.
|
|
945
|
+
if (defaultModel) {
|
|
946
|
+
config.model = defaultModel;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Only include defaultMethod if it's not the default 'llm'
|
|
950
|
+
if (defaultMethod && defaultMethod !== 'llm') {
|
|
951
|
+
config.defaultMethod = defaultMethod;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Only include contentDir if the user specified one
|
|
955
|
+
if (contentDir) {
|
|
956
|
+
config.contentDir = contentDir;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Only include temperature if the user set a non-default value
|
|
960
|
+
if (temperature != null) {
|
|
961
|
+
config.temperature = temperature;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
return config;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Run the interactive init wizard.
|
|
969
|
+
*
|
|
970
|
+
* CLI flags (--langs, --source, --dir, --model, --temperature, --format)
|
|
971
|
+
* prefill the wizard's defaults — Enter accepts them at each step.
|
|
972
|
+
*/
|
|
973
|
+
async function runInteractive(cwd, args = {}) {
|
|
974
|
+
const rl = readline.createInterface({
|
|
975
|
+
input: process.stdin,
|
|
976
|
+
output: process.stdout,
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
try {
|
|
980
|
+
console.log('');
|
|
981
|
+
console.log(' champollion — Project Setup');
|
|
982
|
+
console.log(' ════════════════════════════════════════════════');
|
|
983
|
+
|
|
984
|
+
// Step 1: Languages (includes orthography choices for dual-script locales)
|
|
985
|
+
const { source, languages, scriptChoices } = await stepLanguages(rl, args);
|
|
986
|
+
|
|
987
|
+
// Step 2: Registers — guided tone/formality per language.
|
|
988
|
+
// Comes before method because register choice informs method selection.
|
|
989
|
+
const customRegisters = await stepRegisters(rl, languages);
|
|
990
|
+
|
|
991
|
+
// Step 3: Translation Method
|
|
992
|
+
const { defaultMethod, defaultModel, perLanguage } = await stepMethod(rl, languages, args.model);
|
|
993
|
+
|
|
994
|
+
// Step 4: Temperature
|
|
995
|
+
const temperature = await stepTemperature(rl, defaultMethod, args.temperature);
|
|
996
|
+
|
|
997
|
+
// Step 5: Content Translation
|
|
998
|
+
const { contentDir, format: contentFormat } = await stepContent(rl, cwd);
|
|
999
|
+
|
|
1000
|
+
// Step 6: Locales directory and format
|
|
1001
|
+
// These are simpler questions — asked inline before confirmation
|
|
1002
|
+
console.log('');
|
|
1003
|
+
const localesDir = await ask(rl, 'Locales directory', args.dir || (contentFormat === 'docusaurus' ? './i18n' : './locales'));
|
|
1004
|
+
const format = contentFormat || await ask(rl, 'File format (auto/json/toml/yaml)', args.format || 'auto');
|
|
1005
|
+
|
|
1006
|
+
// Build config
|
|
1007
|
+
const config = buildConfig({
|
|
1008
|
+
source, languages, defaultMethod, defaultModel,
|
|
1009
|
+
perLanguage, customRegisters, temperature,
|
|
1010
|
+
localesDir, format, contentDir, scriptChoices,
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
// Collect env vars needed
|
|
1014
|
+
const envVars = collectRequiredEnvVars(defaultMethod, config.languages);
|
|
1015
|
+
|
|
1016
|
+
// Confirm
|
|
1017
|
+
const confirmed = await stepConfirm(rl, config, envVars);
|
|
1018
|
+
if (!confirmed) {
|
|
1019
|
+
console.log(' Cancelled. No files written.');
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Return config + envVars for the caller to write and show next steps
|
|
1024
|
+
return { config, envVars };
|
|
1025
|
+
} finally {
|
|
1026
|
+
rl.close();
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Generate a default config for non-interactive mode.
|
|
1032
|
+
*
|
|
1033
|
+
* Supports --langs to specify target languages in quick mode:
|
|
1034
|
+
* npx champollion init --yes --langs fr,de,ja
|
|
1035
|
+
*
|
|
1036
|
+
* WHY --langs in quick mode: The user said "define your targets, state
|
|
1037
|
+
* the locales, click sync to use defaults." This flag enables that
|
|
1038
|
+
* one-liner workflow without the interactive wizard.
|
|
1039
|
+
*/
|
|
1040
|
+
async function buildDefaultConfig(args) {
|
|
1041
|
+
// Parse --langs flag: comma-separated list of language codes
|
|
1042
|
+
const languages = args.langs
|
|
1043
|
+
? parseLanguageInput(args.langs)
|
|
1044
|
+
: [];
|
|
1045
|
+
|
|
1046
|
+
// Resolve the model for --yes mode.
|
|
1047
|
+
// If --model is explicitly provided, use it directly.
|
|
1048
|
+
// Otherwise, try to fetch the top model from the provider API.
|
|
1049
|
+
// If no API key or fetch fails, leave model unset (method default fires at runtime).
|
|
1050
|
+
let model = args.model || null;
|
|
1051
|
+
const method = args.method || 'llm';
|
|
1052
|
+
|
|
1053
|
+
if (!model && method !== 'llm' && isListableProvider(method)) {
|
|
1054
|
+
const apiKey = resolveProviderApiKey(method);
|
|
1055
|
+
if (apiKey) {
|
|
1056
|
+
const models = await fetchAvailableModels(method, apiKey);
|
|
1057
|
+
if (models && models.length > 0) {
|
|
1058
|
+
model = models[0]; // Top model from the provider
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// For OpenRouter (default method), use the hardcoded default
|
|
1064
|
+
if (!model && method === 'llm') {
|
|
1065
|
+
model = DEFAULT_OPENROUTER_MODEL;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const config = {
|
|
1069
|
+
version: 3,
|
|
1070
|
+
inputLocale: args.source || 'en',
|
|
1071
|
+
localesDir: args.dir || './locales',
|
|
1072
|
+
languages,
|
|
1073
|
+
batchSize: DEFAULT_BATCH_SIZE,
|
|
1074
|
+
format: args.format || 'auto',
|
|
1075
|
+
...(args.temperature != null && { temperature: parseFloat(args.temperature) }),
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
// Only include method if not the default
|
|
1079
|
+
if (method !== 'llm') {
|
|
1080
|
+
config.defaultMethod = method;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Only write model to config when explicitly known
|
|
1084
|
+
if (model) {
|
|
1085
|
+
config.model = model;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return config;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Validate init flag values before any config is written.
|
|
1093
|
+
* Returns an error message string, or null when the flags are fine.
|
|
1094
|
+
*
|
|
1095
|
+
* WHY fail-fast: agents drive `init --yes` with flags — a typo'd method
|
|
1096
|
+
* or a non-numeric temperature must error loudly here, not surface as a
|
|
1097
|
+
* broken config ("temperature": null) at first sync.
|
|
1098
|
+
*/
|
|
1099
|
+
function validateInitFlags(args) {
|
|
1100
|
+
if (args.method && !METHOD_OPTIONS.some(m => m.method === args.method)) {
|
|
1101
|
+
const valid = METHOD_OPTIONS.map(m => m.method).join(', ');
|
|
1102
|
+
return `Unknown method "${args.method}". Valid methods: ${valid}`;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (args.temperature != null) {
|
|
1106
|
+
const t = parseFloat(args.temperature);
|
|
1107
|
+
if (isNaN(t) || t < 0 || t > 1) {
|
|
1108
|
+
return `Invalid --temperature "${args.temperature}" — must be a number between 0.0 and 1.0.`;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
async function run(args, cwd) {
|
|
1116
|
+
const configPath = path.join(cwd, DEFAULT_CONFIG_FILENAME);
|
|
1117
|
+
|
|
1118
|
+
// ── Help ──
|
|
1119
|
+
// NOTE: `champollion init --help` is normally intercepted by bin/cli.js
|
|
1120
|
+
// before this module loads — this branch covers programmatic callers.
|
|
1121
|
+
// The help text SSOT is COMMAND_HELP.init in lib/command-help.js.
|
|
1122
|
+
if (args.help) {
|
|
1123
|
+
showCommandHelp('init');
|
|
1124
|
+
return 0;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// ── Fail fast on bad flag values (both modes, before anything is written) ──
|
|
1128
|
+
const flagError = validateInitFlags(args);
|
|
1129
|
+
if (flagError) {
|
|
1130
|
+
output.error(flagError);
|
|
1131
|
+
return 1;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// ── Guard: config already exists ──
|
|
1135
|
+
// Without --force, refuse to clobber an existing config. Exit non-zero so
|
|
1136
|
+
// scripts (and the user) can tell "already initialized / not regenerated"
|
|
1137
|
+
// from a successful fresh init — a silent exit 0 here hid the no-op,
|
|
1138
|
+
// especially for a corrupt config that the user expected `init --yes` to fix.
|
|
1139
|
+
// Pass --force to regenerate over the top of an existing (or invalid) config.
|
|
1140
|
+
if (fs.existsSync(configPath) && !args.force) {
|
|
1141
|
+
output.warn(`Config file already exists: ${DEFAULT_CONFIG_FILENAME}`);
|
|
1142
|
+
output.raw(' Run with --force to regenerate, or delete it first.');
|
|
1143
|
+
return 1;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// ── Choose mode: interactive wizard or silent defaults ──
|
|
1147
|
+
let config;
|
|
1148
|
+
let envVars = [];
|
|
1149
|
+
|
|
1150
|
+
if (!args.yes && isInteractive()) {
|
|
1151
|
+
const result = await runInteractive(cwd, args);
|
|
1152
|
+
if (!result) return 0; // User cancelled
|
|
1153
|
+
config = result.config;
|
|
1154
|
+
envVars = result.envVars;
|
|
1155
|
+
} else {
|
|
1156
|
+
// Say WHY the wizard was skipped — an agent (or CI) piping stdin
|
|
1157
|
+
// shouldn't have to guess which mode ran.
|
|
1158
|
+
if (!args.yes) {
|
|
1159
|
+
output.info('stdin is not a TTY — skipping the wizard, writing a default config.');
|
|
1160
|
+
output.raw(' Configure via flags (--langs, --method, --model, ...); see `champollion init --help`.');
|
|
1161
|
+
}
|
|
1162
|
+
config = await buildDefaultConfig(args);
|
|
1163
|
+
envVars = collectRequiredEnvVars(config.defaultMethod || 'llm', config.languages);
|
|
1164
|
+
|
|
1165
|
+
// Typos in --langs write configs that only break at first sync — warn now.
|
|
1166
|
+
for (const code of Array.isArray(config.languages) ? config.languages : []) {
|
|
1167
|
+
if (!getLanguageCard(code) && !DEFAULT_REGISTERS[code]) {
|
|
1168
|
+
output.warn(`Unrecognized language code "${code}" — kept in config, but check the spelling.`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// ── Write config ──
|
|
1174
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
1175
|
+
output.ok(`Created ${DEFAULT_CONFIG_FILENAME}`);
|
|
1176
|
+
output.raw('');
|
|
1177
|
+
|
|
1178
|
+
// ── Show register summary when languages are configured ──
|
|
1179
|
+
const langCount = Array.isArray(config.languages) ? config.languages.length : Object.keys(config.languages).length;
|
|
1180
|
+
if (langCount > 0) {
|
|
1181
|
+
output.raw(' Registers:');
|
|
1182
|
+
if (typeof config.languages === 'object' && !Array.isArray(config.languages)) {
|
|
1183
|
+
// Object form — show preset keys
|
|
1184
|
+
for (const [code, value] of Object.entries(config.languages)) {
|
|
1185
|
+
const card = getLanguageCard(code);
|
|
1186
|
+
const name = card?.name || code;
|
|
1187
|
+
if (typeof value === 'string') {
|
|
1188
|
+
// Bare preset key
|
|
1189
|
+
output.raw(` ${code.padEnd(6)} ${name} → ${value}`);
|
|
1190
|
+
} else if (typeof value === 'object' && value.register) {
|
|
1191
|
+
output.raw(` ${code.padEnd(6)} ${name} → ${value.register}`);
|
|
1192
|
+
} else {
|
|
1193
|
+
output.raw(` ${code.padEnd(6)} ${name} → (default)`);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
} else {
|
|
1197
|
+
// Array form — all defaults
|
|
1198
|
+
for (const code of config.languages) {
|
|
1199
|
+
const card = getLanguageCard(code);
|
|
1200
|
+
const name = card?.name || code;
|
|
1201
|
+
const defaultKey = card?.formality?.default || 'default';
|
|
1202
|
+
output.raw(` ${code.padEnd(6)} ${name} → ${defaultKey}`);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
output.raw('');
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// ── Next steps — skip env var instruction if already set ──
|
|
1209
|
+
output.raw(' Next steps:');
|
|
1210
|
+
output.raw('');
|
|
1211
|
+
let step = 1;
|
|
1212
|
+
|
|
1213
|
+
// Only show env var setup if any required key is missing from the environment
|
|
1214
|
+
const missingEnvVars = envVars.filter(({ envVar }) => !process.env[envVar]);
|
|
1215
|
+
if (missingEnvVars.length === 1) {
|
|
1216
|
+
output.raw(` ${step}. Set your API key:`);
|
|
1217
|
+
output.raw(` export ${missingEnvVars[0].envVar}=...`);
|
|
1218
|
+
step++;
|
|
1219
|
+
} else if (missingEnvVars.length > 1) {
|
|
1220
|
+
output.raw(` ${step}. Set your API key(s):`);
|
|
1221
|
+
for (const { envVar, label } of missingEnvVars) {
|
|
1222
|
+
output.raw(` export ${envVar}=... # ${label}`);
|
|
1223
|
+
}
|
|
1224
|
+
step++;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
output.raw('');
|
|
1228
|
+
if (langCount === 0) {
|
|
1229
|
+
output.raw(` ${step}. Add target locale files to your locales directory (e.g., fr.json)`);
|
|
1230
|
+
output.raw(` ${step + 1}. Run: champollion status # verify your setup`);
|
|
1231
|
+
output.raw(` ${step + 2}. Run: champollion sync # translate!`);
|
|
1232
|
+
} else {
|
|
1233
|
+
output.raw(` ${step}. Run: champollion status # verify your setup`);
|
|
1234
|
+
output.raw(` ${step + 1}. Run: champollion sync # translate!`);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// ── Evidence hint — published results + live engine availability per pair ──
|
|
1238
|
+
if (langCount > 0) {
|
|
1239
|
+
const firstTarget = Array.isArray(config.languages)
|
|
1240
|
+
? config.languages[0]
|
|
1241
|
+
: Object.keys(config.languages)[0];
|
|
1242
|
+
output.raw('');
|
|
1243
|
+
output.raw(' Evidence for your pairs (published results + what’s runnable now):');
|
|
1244
|
+
output.raw(` champollion recommend ${config.inputLocale} ${firstTarget}`);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// ── Cost-saving tips — help users discover TM early ──
|
|
1248
|
+
output.raw('');
|
|
1249
|
+
output.raw(' After your first sync:');
|
|
1250
|
+
output.raw(' champollion tm stats # see cached translations');
|
|
1251
|
+
output.raw(' champollion xliff export --locale fr # export for human review');
|
|
1252
|
+
output.raw('');
|
|
1253
|
+
output.raw(' Translations are cached in .champollion/tm.json \u2014 re-running sync');
|
|
1254
|
+
output.raw(' only calls the API for keys that actually changed.');
|
|
1255
|
+
|
|
1256
|
+
return 0;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
export { run, parseLanguageInput, buildDefaultConfig, buildConfig };
|