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,314 @@
|
|
|
1
|
+
import { TranslationMethod } from './base.js';
|
|
2
|
+
import { getEnvOrFileVar } from '../api-key.js';
|
|
3
|
+
import { DEFAULT_METHOD_CONCURRENCY } from '../config.js';
|
|
4
|
+
import { estimateProviderCost } from './provider-pricing.js';
|
|
5
|
+
import { extractContentBody } from './content-separator.js';
|
|
6
|
+
import { output } from '../output.js';
|
|
7
|
+
import { fetchWithRetry } from './fetch-with-retry.js';
|
|
8
|
+
import { pMap } from '../concurrent.js';
|
|
9
|
+
|
|
10
|
+
const LIBRETRANSLATE_REQUEST_TIMEOUT_MS = 15000;
|
|
11
|
+
|
|
12
|
+
class LibreTranslateMethod extends TranslationMethod {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
super('libretranslate', options);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ── API resolution helpers ──────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the LibreTranslate API endpoint URL.
|
|
21
|
+
* Falls back to localhost:5000 for self-hosted instances.
|
|
22
|
+
* @param {object} options - Caller-provided options
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
_resolveApiEndpoint(options) {
|
|
26
|
+
return options.libretranslateApiUrl
|
|
27
|
+
|| getEnvOrFileVar('LIBRETRANSLATE_API_URL')
|
|
28
|
+
|| getEnvOrFileVar('LIBRETRANSLATE_API_URL', options.cwd)
|
|
29
|
+
|| 'http://localhost:5000/translate';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the LibreTranslate API key (optional for self-hosted).
|
|
34
|
+
* @param {object} options - Caller-provided options
|
|
35
|
+
* @returns {string|null}
|
|
36
|
+
*/
|
|
37
|
+
_resolveApiKey(options) {
|
|
38
|
+
return options.libretranslateApiKey
|
|
39
|
+
|| getEnvOrFileVar('LIBRETRANSLATE_API_KEY')
|
|
40
|
+
|| getEnvOrFileVar('LIBRETRANSLATE_API_KEY', options.cwd);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Translate a batch of key-value pairs via LibreTranslate API.
|
|
45
|
+
*
|
|
46
|
+
* @param {string[]} keys - Flat dot-notation keys to translate
|
|
47
|
+
* @param {object} sourceFlat - Full flattened source locale
|
|
48
|
+
* @param {import('../types.js').PairConfig} pairConfig - Pair config
|
|
49
|
+
* @param {object} options - { apiKey, batchSize }
|
|
50
|
+
* @returns {object|null} Map of key → translated value, or null
|
|
51
|
+
*/
|
|
52
|
+
async translate(keys, sourceFlat, pairConfig, options) {
|
|
53
|
+
const apiEndpoint = this._resolveApiEndpoint(options);
|
|
54
|
+
const apiKey = this._resolveApiKey(options);
|
|
55
|
+
|
|
56
|
+
const targetLocale = pairConfig.target;
|
|
57
|
+
const sourceLocale = pairConfig.source || 'en';
|
|
58
|
+
const allTranslated = {};
|
|
59
|
+
|
|
60
|
+
const maxSegments = pairConfig.batchSize || options.batchSize || 64;
|
|
61
|
+
|
|
62
|
+
const batchChunks = [];
|
|
63
|
+
for (let i = 0; i < keys.length; i += maxSegments) {
|
|
64
|
+
batchChunks.push(keys.slice(i, i + maxSegments));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await pMap(batchChunks, async (chunk, idx) => {
|
|
68
|
+
const orderedKeys = [];
|
|
69
|
+
const sourceTexts = [];
|
|
70
|
+
|
|
71
|
+
for (const key of chunk) {
|
|
72
|
+
const val = sourceFlat[key];
|
|
73
|
+
if (val && typeof val === 'string') {
|
|
74
|
+
orderedKeys.push(key);
|
|
75
|
+
sourceTexts.push(val);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (sourceTexts.length === 0) return;
|
|
80
|
+
|
|
81
|
+
const result = await this._translateBatchWithRetry({
|
|
82
|
+
apiEndpoint,
|
|
83
|
+
apiKey,
|
|
84
|
+
orderedKeys,
|
|
85
|
+
sourceTexts,
|
|
86
|
+
sourceLocale,
|
|
87
|
+
targetLocale,
|
|
88
|
+
batchNum: idx + 1,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (result) {
|
|
92
|
+
Object.assign(allTranslated, result);
|
|
93
|
+
}
|
|
94
|
+
}, { concurrency: DEFAULT_METHOD_CONCURRENCY });
|
|
95
|
+
|
|
96
|
+
return Object.keys(allTranslated).length > 0 ? allTranslated : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Translate freeform Markdown content via LibreTranslate API.
|
|
101
|
+
*
|
|
102
|
+
* Same protect/restore approach — ⟦PROTECTED_N⟧ placeholders shield
|
|
103
|
+
* code blocks and shortcodes from the translation engine.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} prompt - Complete translation prompt from buildContentPrompt()
|
|
106
|
+
* @param {import('../types.js').PairConfig} pairConfig - Pair config
|
|
107
|
+
* @param {object} options - {}
|
|
108
|
+
* @returns {string|null} Translated text, or null on failure
|
|
109
|
+
*/
|
|
110
|
+
async translateContent(prompt, pairConfig, options) {
|
|
111
|
+
const apiEndpoint = this._resolveApiEndpoint(options);
|
|
112
|
+
const apiKey = this._resolveApiKey(options);
|
|
113
|
+
|
|
114
|
+
// Extract the Markdown body from the translation prompt
|
|
115
|
+
const bodyText = extractContentBody(prompt);
|
|
116
|
+
if (!bodyText.trim()) return null;
|
|
117
|
+
|
|
118
|
+
const targetLocale = pairConfig.target;
|
|
119
|
+
const sourceLocale = pairConfig.source || 'en';
|
|
120
|
+
|
|
121
|
+
const body = {
|
|
122
|
+
q: bodyText,
|
|
123
|
+
source: sourceLocale,
|
|
124
|
+
target: targetLocale,
|
|
125
|
+
format: 'text',
|
|
126
|
+
};
|
|
127
|
+
if (apiKey) {
|
|
128
|
+
body.api_key = apiKey;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const response = await fetchWithRetry(apiEndpoint, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'Content-Type': 'application/json' },
|
|
134
|
+
body: JSON.stringify(body),
|
|
135
|
+
}, {
|
|
136
|
+
label: 'LibreTranslate content',
|
|
137
|
+
timeoutMs: LIBRETRANSLATE_REQUEST_TIMEOUT_MS * 2,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!response) return null;
|
|
141
|
+
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
const errorBody = await response.text();
|
|
144
|
+
output.error(`LibreTranslate content: ${response.status} — ${errorBody}`);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const json = await response.json();
|
|
149
|
+
if (!json?.translatedText) {
|
|
150
|
+
output.error('LibreTranslate content: empty response');
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return json.translatedText;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Preflight check: verify the LibreTranslate server is reachable.
|
|
159
|
+
*
|
|
160
|
+
* Unlike cloud APIs that just need a key check, LibreTranslate is
|
|
161
|
+
* self-hosted — the server might not be running. We probe it here
|
|
162
|
+
* so the user gets a clear "server is down" message at startup instead
|
|
163
|
+
* of cryptic fetch errors deep in the translation loop.
|
|
164
|
+
*/
|
|
165
|
+
async checkReadiness(context) {
|
|
166
|
+
const endpointUrl = this._resolveApiEndpoint(context);
|
|
167
|
+
|
|
168
|
+
// The /translate endpoint base — try to reach the API root
|
|
169
|
+
const baseUrl = endpointUrl.replace(/\/translate\/?$/, '');
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const controller = new AbortController();
|
|
173
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
174
|
+
|
|
175
|
+
const response = await fetch(`${baseUrl}/languages`, {
|
|
176
|
+
method: 'GET',
|
|
177
|
+
signal: controller.signal,
|
|
178
|
+
});
|
|
179
|
+
clearTimeout(timeoutId);
|
|
180
|
+
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
return {
|
|
183
|
+
ready: false,
|
|
184
|
+
reason:
|
|
185
|
+
`LibreTranslate server at ${baseUrl} responded with ${response.status}.\n` +
|
|
186
|
+
` Check that LibreTranslate is running and accessible.`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return { ready: true };
|
|
190
|
+
} catch (err) {
|
|
191
|
+
return {
|
|
192
|
+
ready: false,
|
|
193
|
+
reason:
|
|
194
|
+
`Cannot reach LibreTranslate server at ${baseUrl}.\n` +
|
|
195
|
+
` ${err.name === 'AbortError' ? 'Connection timed out.' : err.message}\n` +
|
|
196
|
+
` Start LibreTranslate: docker run -ti --rm -p 5000:5000 libretranslate/libretranslate`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
estimateCost(keyCount) {
|
|
202
|
+
return estimateProviderCost('libretranslate', keyCount);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
getQualityTier() {
|
|
206
|
+
return 'standard';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getProvenance() {
|
|
210
|
+
return {
|
|
211
|
+
resources: [
|
|
212
|
+
{
|
|
213
|
+
name: 'LibreTranslate API',
|
|
214
|
+
license: 'AGPL-3.0 (Self-hosted)',
|
|
215
|
+
type: 'api',
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
// AGPL-3.0 — matches shared/method-registry.json (commercialReady:
|
|
219
|
+
// false) and docs/MT_SYSTEMS_UNDER_TEST.md. This said `true` until
|
|
220
|
+
// 2026-08-12 while declaring AGPL in the same object; the CLAUDE.md
|
|
221
|
+
// AGPL boundary makes that a licence-boundary defect, not a nuance.
|
|
222
|
+
// Non-commercial use is unaffected — that lane is not gated.
|
|
223
|
+
commercialReady: false,
|
|
224
|
+
flags: ['COPYLEFT_AGPL'],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
getSetupHelp() {
|
|
229
|
+
// LibreTranslate is self-hosted — there's no "missing key" case,
|
|
230
|
+
// just a "can't connect" case. Always show the connection guide.
|
|
231
|
+
return [
|
|
232
|
+
'',
|
|
233
|
+
' ┌─ LibreTranslate Connection Failed ──────────────────────────────┐',
|
|
234
|
+
' │ Could not connect to LibreTranslate API. │',
|
|
235
|
+
' │ │',
|
|
236
|
+
' │ Default endpoint: http://localhost:5000/translate │',
|
|
237
|
+
' │ │',
|
|
238
|
+
' │ To use a remote instance: │',
|
|
239
|
+
' │ export LIBRETRANSLATE_API_URL=https://your-instance/translate │',
|
|
240
|
+
' │ export LIBRETRANSLATE_API_KEY=... (if required) │',
|
|
241
|
+
' │ │',
|
|
242
|
+
' │ Self-host: docker run -p 5000:5000 libretranslate/libretranslate │',
|
|
243
|
+
' └──────────────────────────────────────────────────────────────────┘',
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async _translateBatchWithRetry({
|
|
248
|
+
apiEndpoint,
|
|
249
|
+
apiKey,
|
|
250
|
+
orderedKeys,
|
|
251
|
+
sourceTexts,
|
|
252
|
+
sourceLocale,
|
|
253
|
+
targetLocale,
|
|
254
|
+
batchNum,
|
|
255
|
+
}) {
|
|
256
|
+
const body = {
|
|
257
|
+
q: sourceTexts,
|
|
258
|
+
source: sourceLocale,
|
|
259
|
+
target: targetLocale,
|
|
260
|
+
format: 'text',
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
if (apiKey) {
|
|
264
|
+
body.api_key = apiKey;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const response = await fetchWithRetry(apiEndpoint, {
|
|
268
|
+
method: 'POST',
|
|
269
|
+
headers: {
|
|
270
|
+
'Content-Type': 'application/json',
|
|
271
|
+
},
|
|
272
|
+
body: JSON.stringify(body),
|
|
273
|
+
}, {
|
|
274
|
+
label: `LibreTranslate batch ${batchNum}`,
|
|
275
|
+
timeoutMs: LIBRETRANSLATE_REQUEST_TIMEOUT_MS,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (!response) return null;
|
|
279
|
+
|
|
280
|
+
if (!response.ok) {
|
|
281
|
+
const errorBody = await response.text();
|
|
282
|
+
output.error(`LibreTranslate batch ${batchNum}: ${response.status} — ${errorBody}`);
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const json = await response.json();
|
|
287
|
+
const translatedText = json?.translatedText;
|
|
288
|
+
|
|
289
|
+
if (!translatedText || (Array.isArray(translatedText) && translatedText.length !== orderedKeys.length)) {
|
|
290
|
+
output.error(`LibreTranslate batch ${batchNum}: Response format or length mismatch`);
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const result = {};
|
|
295
|
+
if (Array.isArray(translatedText)) {
|
|
296
|
+
for (let i = 0; i < orderedKeys.length; i++) {
|
|
297
|
+
result[orderedKeys[i]] = translatedText[i];
|
|
298
|
+
}
|
|
299
|
+
} else if (orderedKeys.length === 1 && typeof translatedText === 'string') {
|
|
300
|
+
// If LibreTranslate returned a single string for a single item batch
|
|
301
|
+
result[orderedKeys[0]] = translatedText;
|
|
302
|
+
} else {
|
|
303
|
+
output.error(`LibreTranslate batch ${batchNum}: Unexpected translatedText format`);
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const charCount = sourceTexts.reduce((sum, t) => sum + t.length, 0);
|
|
308
|
+
output.progress(` ✓ LibreTranslate batch ${batchNum} (${orderedKeys.length} keys, ${charCount} chars)`);
|
|
309
|
+
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export { LibreTranslateMethod };
|