champollion 0.3.3 → 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.
Files changed (142) hide show
  1. package/README.md +52 -37
  2. package/bin/cli.js +53 -5
  3. package/index.js +63 -2
  4. package/lib/api-key.js +17 -4
  5. package/lib/autofix.js +83 -36
  6. package/lib/bridge/method_bridge.py +15 -3
  7. package/lib/cards/reader.js +51 -3
  8. package/lib/cards/remote.js +15 -0
  9. package/lib/cards/search-names.js +178 -0
  10. package/lib/command-help.js +289 -88
  11. package/lib/commands/audit.js +10 -3
  12. package/lib/commands/card.js +583 -226
  13. package/lib/commands/doctor.js +54 -18
  14. package/lib/commands/help.js +37 -32
  15. package/lib/commands/init.js +1689 -87
  16. package/lib/commands/integrity.js +127 -40
  17. package/lib/commands/leaderboard.js +187 -67
  18. package/lib/commands/models.js +9 -2
  19. package/lib/commands/provenance.js +7 -2
  20. package/lib/commands/recommend.js +43 -14
  21. package/lib/commands/register-corpus.js +649 -130
  22. package/lib/commands/seal-corpus.js +1 -1
  23. package/lib/commands/status.js +564 -27
  24. package/lib/commands/submit.js +17 -12
  25. package/lib/commands/sync.js +31 -7
  26. package/lib/commands/tm.js +16 -10
  27. package/lib/commands/verify.js +27 -3
  28. package/lib/commands/wrap.js +63 -5
  29. package/lib/commands/xliff.js +135 -64
  30. package/lib/commercial-eligibility.js +1 -1
  31. package/lib/config.js +196 -14
  32. package/lib/content-estimate.js +96 -0
  33. package/lib/content-refusals.js +270 -0
  34. package/lib/content-review.js +372 -0
  35. package/lib/content-sync.js +1127 -344
  36. package/lib/content.js +94 -7
  37. package/lib/corpus-registration.mjs +197 -38
  38. package/lib/cost-label.js +29 -0
  39. package/lib/cost-report.js +726 -78
  40. package/lib/diff.js +38 -4
  41. package/lib/docusaurus-sync.js +965 -253
  42. package/lib/edit-distance.js +31 -0
  43. package/lib/fallback.js +964 -0
  44. package/lib/file-scope.js +106 -0
  45. package/lib/flatten.js +80 -3
  46. package/lib/flutter-locales.js +124 -0
  47. package/lib/format.js +266 -12
  48. package/lib/hash.js +146 -21
  49. package/lib/icu-structure.js +929 -0
  50. package/lib/integrity.js +223 -75
  51. package/lib/language-pair.js +157 -0
  52. package/lib/lint.js +78 -16
  53. package/lib/local-only-marks.js +106 -0
  54. package/lib/locale-layout.js +1103 -0
  55. package/lib/locale-state.js +571 -0
  56. package/lib/methods/anthropic.js +5 -0
  57. package/lib/methods/apertium.js +6 -3
  58. package/lib/methods/api.js +138 -25
  59. package/lib/methods/base.js +17 -0
  60. package/lib/methods/coaching-data.js +153 -0
  61. package/lib/methods/content-separator.js +43 -0
  62. package/lib/methods/deepl.js +1 -1
  63. package/lib/methods/direct-llm.js +252 -103
  64. package/lib/methods/external.js +146 -63
  65. package/lib/methods/gemini.js +1 -0
  66. package/lib/methods/google-translate.js +1 -0
  67. package/lib/methods/http-utils.js +41 -0
  68. package/lib/methods/libretranslate.js +7 -2
  69. package/lib/methods/llm-coached.js +68 -128
  70. package/lib/methods/llm.js +80 -31
  71. package/lib/methods/local.js +93 -10
  72. package/lib/methods/microsoft-translator.js +1 -2
  73. package/lib/methods/openai.js +4 -2
  74. package/lib/methods/openrouter-client.js +20 -19
  75. package/lib/methods/openrouter-pricing.js +150 -13
  76. package/lib/methods/prompt-methods.js +20 -0
  77. package/lib/methods/provider-pricing.js +42 -1
  78. package/lib/methods/request-capture.js +104 -0
  79. package/lib/methods/tilde.js +1 -1
  80. package/lib/methods/translated.js +1 -2
  81. package/lib/missing-key.js +93 -0
  82. package/lib/models.js +11 -0
  83. package/lib/name-rules.js +32 -0
  84. package/lib/named-keys.js +172 -0
  85. package/lib/no-translate.js +4 -3
  86. package/lib/output.js +160 -19
  87. package/lib/pairs.js +586 -30
  88. package/lib/placeholders.js +394 -0
  89. package/lib/plugins.js +8 -0
  90. package/lib/plural-gap-redo.js +109 -0
  91. package/lib/plurals.js +323 -0
  92. package/lib/po.js +1187 -0
  93. package/lib/public-catalogue.js +74 -0
  94. package/lib/recommend.js +527 -32
  95. package/lib/redo.js +95 -0
  96. package/lib/refusal-category.js +44 -0
  97. package/lib/registers.js +255 -11
  98. package/lib/repair-script.js +20 -13
  99. package/lib/scripts.js +193 -106
  100. package/lib/seal.mjs +6 -5
  101. package/lib/sealed-qualifier.mjs +2 -2
  102. package/lib/segment.js +2 -1
  103. package/lib/seo.js +19 -9
  104. package/lib/serve.js +43 -6
  105. package/lib/shared-output-seed.js +164 -0
  106. package/lib/source-contexts.js +39 -0
  107. package/lib/submit.mjs +57 -5
  108. package/lib/sync.js +2923 -474
  109. package/lib/terminology.js +13 -4
  110. package/lib/tm-evict.js +179 -0
  111. package/lib/tm-seed.js +5 -2
  112. package/lib/tm.js +818 -36
  113. package/lib/translate-pair.js +639 -34
  114. package/lib/translate.js +78 -5
  115. package/lib/types.js +22 -3
  116. package/lib/validate.js +880 -17
  117. package/lib/verify.js +1296 -104
  118. package/lib/watch.js +32 -13
  119. package/lib/xliff.js +44 -3
  120. package/package.json +3 -2
  121. package/shared/CORPORA-CARDS.md +2 -0
  122. package/shared/DATA-SOVEREIGNTY.md +19 -20
  123. package/shared/LANGUAGE-CARD-FIELDS.md +1 -1
  124. package/shared/cards-fallback.json +1 -1
  125. package/shared/catalogue/card-config.json +1 -1
  126. package/shared/curated-orthography-conventions.json +26 -8
  127. package/shared/docent/faq.en.json +14 -16
  128. package/shared/docent/system-prompt.md +17 -19
  129. package/shared/explainers/tc-features.json +15 -15
  130. package/shared/gettext-plural-forms.json +45 -0
  131. package/shared/human-services.json +1 -1
  132. package/shared/method-registry.json +2 -0
  133. package/shared/metric-registry.json +96 -18
  134. package/shared/schemas/champollion-plugin.schema.json +4 -0
  135. package/shared/schemas/corpora-card.schema.json +20 -10
  136. package/shared/schemas/human-services.schema.json +2 -2
  137. package/shared/schemas/language-card.schema.json +1 -1
  138. package/shared/schemas/method-card.schema.json +1 -1
  139. package/shared/schemas/method-index-record.schema.json +67 -0
  140. package/shared/schemas/method-registry.schema.json +4 -0
  141. package/shared/schemas/metric-registry.schema.json +55 -1
  142. package/shared/docent/corpus.json +0 -11333
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * This is the core Champollion deployment pipeline for custom methods:
5
5
  *
6
- * 1. pip install crk-translate (or quechua-translate, or any module)
6
+ * 1. python3 -m pip install crk-translate (or quechua-translate, or any module)
7
7
  * 2. champollion init → wizard detects the module, offers it
8
8
  * 3. champollion sync → bridge spawns, translates, done
9
9
  *
@@ -51,6 +51,55 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
51
51
  */
52
52
  const BRIDGE_SCRIPT = resolve(__dirname, '..', 'bridge', 'method_bridge.py');
53
53
 
54
+ /** Interpreter names tried in order (python3 on macOS/Linux, python on Windows). */
55
+ const PYTHON_CANDIDATES = ['python3', 'python'];
56
+
57
+ /**
58
+ * Spawn the first Python interpreter that exists.
59
+ *
60
+ * spawn() reports a missing executable ASYNCHRONOUSLY, as an 'error' event
61
+ * with code ENOENT — a try/catch around spawn() never sees it. So wait for
62
+ * 'spawn' (it started) or 'error' (it didn't), and move to the next name only
63
+ * on ENOENT. Rejects with an actionable message when none exists.
64
+ *
65
+ * @param {string[]} args
66
+ * @param {import('node:child_process').SpawnOptions} options
67
+ * @returns {Promise<import('node:child_process').ChildProcess>}
68
+ */
69
+ function spawnPython(args, options) {
70
+ return new Promise((resolvePromise, reject) => {
71
+ const attempt = (i) => {
72
+ const proc = spawn(PYTHON_CANDIDATES[i], args, options);
73
+ const onSpawn = () => {
74
+ proc.removeListener('error', onError);
75
+ resolvePromise(proc);
76
+ };
77
+ const onError = (err) => {
78
+ proc.removeListener('spawn', onSpawn);
79
+ if (err.code !== 'ENOENT') return reject(err);
80
+ if (i + 1 < PYTHON_CANDIDATES.length) return attempt(i + 1);
81
+ reject(new Error(
82
+ `Python is required for external method modules but was not found ` +
83
+ `(tried ${PYTHON_CANDIDATES.join(', ')} on PATH).\n` +
84
+ ' Install Python 3.10+ from https://python.org'
85
+ ));
86
+ };
87
+ proc.once('spawn', onSpawn);
88
+ proc.once('error', onError);
89
+ };
90
+ attempt(0);
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Bridges alive in this process. Killed on exit as a backstop, so no Python
96
+ * child outlives the CLI even on a path that never called shutdown().
97
+ */
98
+ const liveBridges = new Set();
99
+ process.once('exit', () => {
100
+ for (const proc of liveBridges) proc.kill();
101
+ });
102
+
54
103
  class ExternalMethod extends TranslationMethod {
55
104
  constructor(options = {}) {
56
105
  super('external', options);
@@ -58,6 +107,13 @@ class ExternalMethod extends TranslationMethod {
58
107
  this._process = null;
59
108
  this._rl = null;
60
109
  this._methodName = null;
110
+ // In-flight bridge start, shared by concurrent callers so they don't each
111
+ // spawn a process.
112
+ this._starting = null;
113
+ // The bridge answers one JSON line per request, in order. Requests are
114
+ // chained so concurrent callers (pairs translating in parallel on one
115
+ // shared instance) can never read each other's response line.
116
+ this._queue = Promise.resolve();
61
117
  }
62
118
 
63
119
  /**
@@ -69,7 +125,13 @@ class ExternalMethod extends TranslationMethod {
69
125
  */
70
126
  async _ensureBridge(cwd) {
71
127
  if (this._process) return;
128
+ if (!this._starting) {
129
+ this._starting = this._startBridge(cwd).finally(() => { this._starting = null; });
130
+ }
131
+ await this._starting;
132
+ }
72
133
 
134
+ async _startBridge(cwd) {
73
135
  const methodPath = this._methodPath
74
136
  ? resolve(cwd || '.', this._methodPath)
75
137
  : null;
@@ -77,42 +139,43 @@ class ExternalMethod extends TranslationMethod {
77
139
  const args = [BRIDGE_SCRIPT];
78
140
  if (methodPath) args.push('--method', methodPath);
79
141
 
80
- // Try python3 first (macOS/Linux convention), then python (Windows)
81
- let pythonCmd = 'python3';
82
- try {
83
- this._process = spawn(pythonCmd, args, {
84
- stdio: ['pipe', 'pipe', 'pipe'],
85
- cwd: cwd || process.cwd(),
86
- });
87
- } catch {
88
- pythonCmd = 'python';
89
- this._process = spawn(pythonCmd, args, {
90
- stdio: ['pipe', 'pipe', 'pipe'],
91
- cwd: cwd || process.cwd(),
92
- });
93
- }
94
-
95
- // Handle spawn failure (Python not installed)
96
- this._process.on('error', (err) => {
97
- if (err.code === 'ENOENT') {
98
- throw new Error(
99
- 'Python is required for external method modules but was not found.\n' +
100
- ' Install Python 3.10+ from https://python.org'
101
- );
142
+ const proc = await spawnPython(args, {
143
+ stdio: ['pipe', 'pipe', 'pipe'],
144
+ cwd: cwd || process.cwd(),
145
+ });
146
+ this._process = proc;
147
+ liveBridges.add(proc);
148
+
149
+ // A later process error (e.g. EPIPE) must not become an uncaught
150
+ // exception; pending reads reject via _readResponse's listeners.
151
+ proc.on('error', (err) => output.warn(` [method] bridge error: ${err.message}`));
152
+ // A dead bridge is forgotten so the next request starts a fresh one
153
+ // instead of writing into a closed pipe.
154
+ proc.once('exit', () => {
155
+ liveBridges.delete(proc);
156
+ if (this._process === proc) {
157
+ this._process = null;
158
+ this._rl = null;
102
159
  }
103
- throw err;
104
160
  });
105
161
 
106
162
  // Python logging → CLI info channel (stderr is for logs, not errors)
107
- const stderrRl = createInterface({ input: this._process.stderr });
163
+ const stderrRl = createInterface({ input: proc.stderr });
108
164
  stderrRl.on('line', (line) => output.info(` [method] ${line}`));
109
165
 
110
166
  // stdout → JSON-lines protocol
111
- this._rl = createInterface({ input: this._process.stdout });
167
+ this._rl = createInterface({ input: proc.stdout });
112
168
 
113
169
  // Wait for the ready signal (first line of output)
114
- const ready = await this._readResponse();
170
+ let ready;
171
+ try {
172
+ ready = await this._readResponse();
173
+ } catch (err) {
174
+ proc.kill();
175
+ throw err;
176
+ }
115
177
  if (!ready.ok) {
178
+ proc.kill();
116
179
  throw new Error(ready.error);
117
180
  }
118
181
  this._methodName = ready.name || 'External Method';
@@ -123,41 +186,62 @@ class ExternalMethod extends TranslationMethod {
123
186
  * Send a JSON request line and read the response line.
124
187
  */
125
188
  async _request(obj) {
126
- return new Promise((resolve, reject) => {
189
+ const send = () => new Promise((resolve, reject) => {
190
+ if (!this._process) {
191
+ reject(new Error('Method bridge is not running.'));
192
+ return;
193
+ }
194
+ // Arm the reader before writing so the response line can't slip past.
195
+ const response = this._readResponse();
196
+ response.then(resolve, reject);
127
197
  const line = JSON.stringify(obj) + '\n';
128
198
  this._process.stdin.write(line, 'utf-8', (err) => {
129
- if (err) return reject(err);
130
- this._readResponse().then(resolve).catch(reject);
199
+ if (err) reject(err);
131
200
  });
132
201
  });
202
+ const result = this._queue.then(send, send);
203
+ this._queue = result.catch(() => {});
204
+ return result;
133
205
  }
134
206
 
135
207
  /**
136
208
  * Read one JSON-lines response from the bridge subprocess.
137
209
  */
138
210
  _readResponse() {
211
+ const rl = this._rl;
212
+ const proc = this._process;
139
213
  return new Promise((resolve, reject) => {
214
+ const cleanup = () => {
215
+ rl.removeListener('line', onLine);
216
+ proc.removeListener('exit', onExit);
217
+ proc.removeListener('error', onError);
218
+ };
140
219
  const onLine = (line) => {
220
+ cleanup();
141
221
  try {
142
222
  resolve(JSON.parse(line));
143
223
  } catch (e) {
144
224
  reject(new Error(`Invalid response from method bridge: ${line}`));
145
225
  }
146
- this._rl.removeListener('line', onLine);
147
- this._process.removeListener('exit', onExit);
148
226
  };
149
227
 
150
228
  const onExit = (code) => {
151
- this._rl.removeListener('line', onLine);
229
+ cleanup();
152
230
  reject(new Error(
153
231
  `Method bridge exited unexpectedly (code ${code}).\n` +
154
232
  ' Check that the method module is installed correctly.\n' +
155
- ' Verify the method module is pip-installed (e.g., pip install <package-name>).'
233
+ ' Verify the method module is installed (e.g., python3 -m pip install <package-name>).'
156
234
  ));
157
235
  };
158
236
 
159
- this._rl.on('line', onLine);
160
- this._process.once('exit', onExit);
237
+ const onError = (err) => {
238
+ cleanup();
239
+ reject(err);
240
+ };
241
+
242
+ rl.on('line', onLine);
243
+ proc.once('exit', onExit);
244
+ proc.once('error', onError);
161
245
  });
162
246
  }
163
247
 
@@ -173,26 +257,18 @@ class ExternalMethod extends TranslationMethod {
173
257
  * @returns {Promise<Array<{name, method_id, supported_pairs, entry_point, plugin_path}>>}
174
258
  */
175
259
  static async discoverMethods(cwd) {
176
- return new Promise((resolve) => {
177
- let proc;
178
- try {
179
- proc = spawn('python3', [BRIDGE_SCRIPT, '--discover'], {
180
- stdio: ['pipe', 'pipe', 'pipe'],
181
- cwd: cwd || process.cwd(),
182
- });
183
- } catch {
184
- try {
185
- proc = spawn('python', [BRIDGE_SCRIPT, '--discover'], {
186
- stdio: ['pipe', 'pipe', 'pipe'],
187
- cwd: cwd || process.cwd(),
188
- });
189
- } catch {
190
- // Python not available — no method modules discoverable
191
- resolve([]);
192
- return;
193
- }
194
- }
260
+ let proc;
261
+ try {
262
+ proc = await spawnPython([BRIDGE_SCRIPT, '--discover'], {
263
+ stdio: ['pipe', 'pipe', 'pipe'],
264
+ cwd: cwd || process.cwd(),
265
+ });
266
+ } catch {
267
+ // Python not available — no method modules discoverable
268
+ return [];
269
+ }
195
270
 
271
+ return new Promise((resolve) => {
196
272
  let data = '';
197
273
  proc.stdout.on('data', (chunk) => { data += chunk; });
198
274
 
@@ -307,7 +383,7 @@ class ExternalMethod extends TranslationMethod {
307
383
  getSetupHelp() {
308
384
  return [
309
385
  ' External method module.',
310
- ' 1. Install a method module (e.g., pip install <module-name>)',
386
+ ' 1. Install a method module (e.g., python3 -m pip install <module-name>)',
311
387
  ' 2. Run: champollion init',
312
388
  ' 3. The wizard will detect and configure it automatically.',
313
389
  ];
@@ -317,14 +393,21 @@ class ExternalMethod extends TranslationMethod {
317
393
  * Clean up: send shutdown command and kill the subprocess.
318
394
  */
319
395
  async shutdown() {
320
- if (this._process) {
321
- try {
322
- await this._request({ action: 'shutdown' });
323
- } catch {
324
- // Process may already be dead — that's fine
325
- }
326
- this._process.kill();
396
+ if (this._starting) {
397
+ try { await this._starting; } catch { /* never started — nothing to stop */ }
398
+ }
399
+ const proc = this._process;
400
+ if (!proc) return;
401
+ try {
402
+ await this._request({ action: 'shutdown' });
403
+ } catch {
404
+ // Process may already be dead — that's fine
405
+ }
406
+ proc.kill();
407
+ liveBridges.delete(proc);
408
+ if (this._process === proc) {
327
409
  this._process = null;
410
+ this._rl = null;
328
411
  }
329
412
  }
330
413
  }
@@ -32,6 +32,7 @@ class GeminiMethod extends DirectLLMMethod {
32
32
  _getApiKeyOptionsKey() { return 'geminiApiKey'; }
33
33
  _getDefaultModel() { return DEFAULT_MODEL; }
34
34
  _getProviderLabel() { return 'Gemini'; }
35
+ _getModelVendor() { return 'google'; }
35
36
 
36
37
  // ── API request/response shape ───────────────────────────────────
37
38
 
@@ -43,6 +43,7 @@ const GOOGLE_REQUEST_TIMEOUT_MS = 15000;
43
43
  class GoogleTranslateMethod extends TranslationMethod {
44
44
  constructor(options = {}) {
45
45
  super('google-translate', options);
46
+ this.translatesRawText = true; // see base.js
46
47
  }
47
48
 
48
49
  // ── API resolution helpers ──────────────────────────────────────
@@ -111,7 +111,48 @@ function stripCodeFences(text) {
111
111
  .trim();
112
112
  }
113
113
 
114
+ /**
115
+ * True only when an endpoint provably stays on this machine — the same rule
116
+ * as the harness's LocalProvider.is_loopback_endpoint(): unix sockets,
117
+ * `localhost` (and `*.localhost`), loopback IPs (127.0.0.0/8, ::1) and the
118
+ * unspecified addresses 0.0.0.0 / :: (a client connecting to them reaches
119
+ * this host). A LAN or private-range address is another machine: false.
120
+ *
121
+ * Used to price a model served here as "$0 API cost (runs on this machine)"
122
+ * instead of "unknown" — never for an endpoint that only might be local.
123
+ *
124
+ * @param {string|null|undefined} url
125
+ * @returns {boolean}
126
+ */
127
+ function isLoopbackEndpoint(url) {
128
+ if (!url || typeof url !== 'string') return false;
129
+ if (/^(?:http\+)?unix:/i.test(url)) return true;
130
+ let host;
131
+ try { host = new URL(url.includes('//') ? url : `http://${url}`).hostname; } catch { return false; }
132
+ host = host.replace(/^\[|\]$/g, '').toLowerCase();
133
+ if (!host) return false;
134
+ if (host === 'localhost' || host.endsWith('.localhost')) return true;
135
+ if (/^127(?:\.\d{1,3}){3}$/.test(host)) return host.split('.').every(o => Number(o) <= 255);
136
+ if (host === '0.0.0.0' || host === '::1' || host === '::') return true;
137
+ // IPv4-mapped IPv6 loopback (::ffff:127.0.0.1 → URL writes ::ffff:7f00:1).
138
+ if (/^::ffff:(?:127(?:\.\d{1,3}){3}|7f[0-9a-f]{2}:[0-9a-f]{1,4})$/.test(host)) return true;
139
+ return false;
140
+ }
141
+
142
+ /** The cost estimate for a model served on this machine (see isLoopbackEndpoint). */
143
+ function localMachineCost(endpoint) {
144
+ return {
145
+ estimatedCost: 0,
146
+ currency: 'USD',
147
+ source: 'local-machine',
148
+ local: true,
149
+ note: `$0 API cost — runs on this machine (${endpoint}); your own hardware and power are not counted.`,
150
+ };
151
+ }
152
+
114
153
  export {
154
+ isLoopbackEndpoint,
155
+ localMachineCost,
115
156
  MAX_RETRIES,
116
157
  BASE_DELAY_MS,
117
158
  REQUEST_TIMEOUT_MS,
@@ -12,6 +12,7 @@ const LIBRETRANSLATE_REQUEST_TIMEOUT_MS = 15000;
12
12
  class LibreTranslateMethod extends TranslationMethod {
13
13
  constructor(options = {}) {
14
14
  super('libretranslate', options);
15
+ this.translatesRawText = true; // see base.js
15
16
  }
16
17
 
17
18
  // ── API resolution helpers ──────────────────────────────────────
@@ -24,7 +25,6 @@ class LibreTranslateMethod extends TranslationMethod {
24
25
  */
25
26
  _resolveApiEndpoint(options) {
26
27
  return options.libretranslateApiUrl
27
- || getEnvOrFileVar('LIBRETRANSLATE_API_URL')
28
28
  || getEnvOrFileVar('LIBRETRANSLATE_API_URL', options.cwd)
29
29
  || 'http://localhost:5000/translate';
30
30
  }
@@ -36,7 +36,6 @@ class LibreTranslateMethod extends TranslationMethod {
36
36
  */
37
37
  _resolveApiKey(options) {
38
38
  return options.libretranslateApiKey
39
- || getEnvOrFileVar('LIBRETRANSLATE_API_KEY')
40
39
  || getEnvOrFileVar('LIBRETRANSLATE_API_KEY', options.cwd);
41
40
  }
42
41
 
@@ -181,6 +180,10 @@ class LibreTranslateMethod extends TranslationMethod {
181
180
  if (!response.ok) {
182
181
  return {
183
182
  ready: false,
183
+ // A server that is down or failing: sync decides after its plan
184
+ // whether this run needs it (lib/sync.js deferred probes).
185
+ unreachable: true,
186
+ endpoint: baseUrl,
184
187
  reason:
185
188
  `LibreTranslate server at ${baseUrl} responded with ${response.status}.\n` +
186
189
  ` Check that LibreTranslate is running and accessible.`,
@@ -190,6 +193,8 @@ class LibreTranslateMethod extends TranslationMethod {
190
193
  } catch (err) {
191
194
  return {
192
195
  ready: false,
196
+ unreachable: true,
197
+ endpoint: baseUrl,
193
198
  reason:
194
199
  `Cannot reach LibreTranslate server at ${baseUrl}.\n` +
195
200
  ` ${err.name === 'AbortError' ? 'Connection timed out.' : err.message}\n` +