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,331 @@
|
|
|
1
|
+
import { TranslationMethod } from './base.js';
|
|
2
|
+
import { getEnvOrFileVar } from '../api-key.js';
|
|
3
|
+
import { resolveProviderEnv, envNamesLabel } from './provider-env.js';
|
|
4
|
+
import { EST_CHARS_PER_KEY, DEFAULT_METHOD_CONCURRENCY } from '../config.js';
|
|
5
|
+
import { estimateProviderCost } from './provider-pricing.js';
|
|
6
|
+
import { extractContentBody } from './content-separator.js';
|
|
7
|
+
import { output } from '../output.js';
|
|
8
|
+
import { fetchWithRetry } from './fetch-with-retry.js';
|
|
9
|
+
import { pMap } from '../concurrent.js';
|
|
10
|
+
|
|
11
|
+
// Global default endpoint base (no query string). A custom-domain / VNet /
|
|
12
|
+
// private-endpoint Azure resource overrides this via MICROSOFT_TRANSLATOR_ENDPOINT
|
|
13
|
+
// (see _resolveEndpoint). Kept separate from the api-version so we build the
|
|
14
|
+
// query the same way the Python harness adapter does
|
|
15
|
+
// (arena/mt_eval_harness/methods/microsoft_translator.py — _resolve_endpoint /
|
|
16
|
+
// _build_request), so a sovereign/private deployment that works there also
|
|
17
|
+
// works here.
|
|
18
|
+
const MICROSOFT_API_BASE = 'https://api.cognitive.microsofttranslator.com/translate';
|
|
19
|
+
const MICROSOFT_API_VERSION = '3.0';
|
|
20
|
+
const MICROSOFT_REQUEST_TIMEOUT_MS = 15000;
|
|
21
|
+
|
|
22
|
+
class MicrosoftTranslatorMethod extends TranslationMethod {
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
super('microsoft-translator', options);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── API resolution helpers ──────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the Microsoft Translator API key from options, env vars, or .env files.
|
|
31
|
+
* Checks the provider-env SSOT: MICROSOFT_TRANSLATOR_API_KEY (canonical),
|
|
32
|
+
* then AZURE_TRANSLATOR_KEY (alias). Accepting the alias here is what keeps
|
|
33
|
+
* the loader in step with readiness/doctor — a user who set only
|
|
34
|
+
* AZURE_TRANSLATOR_KEY now actually runs instead of silently producing null.
|
|
35
|
+
* @param {object} options - Caller-provided options
|
|
36
|
+
* @returns {string|null}
|
|
37
|
+
*/
|
|
38
|
+
_resolveApiKey(options) {
|
|
39
|
+
return options.microsoftApiKey
|
|
40
|
+
|| resolveProviderEnv('microsoft-translator', options.cwd).value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the Azure region for the Translator resource.
|
|
45
|
+
* @param {object} options - Caller-provided options
|
|
46
|
+
* @returns {string|null}
|
|
47
|
+
*/
|
|
48
|
+
_resolveRegion(options) {
|
|
49
|
+
return options.microsoftRegion
|
|
50
|
+
|| getEnvOrFileVar('MICROSOFT_TRANSLATOR_REGION')
|
|
51
|
+
|| getEnvOrFileVar('MICROSOFT_TRANSLATOR_REGION', options.cwd);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the Microsoft Translator /translate endpoint.
|
|
56
|
+
*
|
|
57
|
+
* Defaults to the global endpoint. A custom-domain / VNet / private-endpoint
|
|
58
|
+
* (sovereign) resource sets MICROSOFT_TRANSLATOR_ENDPOINT to its own URL —
|
|
59
|
+
* accepted either as a host base ("https://my-res.cognitiveservices.azure.com")
|
|
60
|
+
* OR a full ".../translate" path; both normalize to the same value. This is
|
|
61
|
+
* the SSOT env var the manifest declares and the Python harness honors
|
|
62
|
+
* (_resolve_endpoint); without this the CLI silently hit the global server for
|
|
63
|
+
* a private deployment.
|
|
64
|
+
* @param {object} options - Caller-provided options
|
|
65
|
+
* @returns {string} normalized endpoint base (no query string)
|
|
66
|
+
*/
|
|
67
|
+
_resolveEndpoint(options = {}) {
|
|
68
|
+
let ep = options.microsoftEndpoint
|
|
69
|
+
|| getEnvOrFileVar('MICROSOFT_TRANSLATOR_ENDPOINT')
|
|
70
|
+
|| getEnvOrFileVar('MICROSOFT_TRANSLATOR_ENDPOINT', options.cwd);
|
|
71
|
+
if (!ep) return MICROSOFT_API_BASE;
|
|
72
|
+
ep = ep.replace(/\/+$/, '');
|
|
73
|
+
if (!ep.endsWith('/translate')) ep = `${ep}/translate`;
|
|
74
|
+
return ep;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build the full request URL: <endpoint>?api-version=3.0&from=<src>&to=<tgt>.
|
|
79
|
+
* @param {string} endpoint - Resolved endpoint base (from _resolveEndpoint)
|
|
80
|
+
* @param {string} sourceLocale
|
|
81
|
+
* @param {string} targetLocale
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
_buildUrl(endpoint, sourceLocale, targetLocale) {
|
|
85
|
+
const params = new URLSearchParams({
|
|
86
|
+
'api-version': MICROSOFT_API_VERSION,
|
|
87
|
+
from: sourceLocale,
|
|
88
|
+
to: targetLocale,
|
|
89
|
+
});
|
|
90
|
+
return `${endpoint}?${params.toString()}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Translate a batch of key-value pairs via Microsoft Translator API.
|
|
95
|
+
*
|
|
96
|
+
* @param {string[]} keys - Flat dot-notation keys to translate
|
|
97
|
+
* @param {object} sourceFlat - Full flattened source locale
|
|
98
|
+
* @param {import('../types.js').PairConfig} pairConfig - Pair config
|
|
99
|
+
* @param {object} options - { apiKey, batchSize }
|
|
100
|
+
* @returns {object|null} Map of key → translated value, or null
|
|
101
|
+
*/
|
|
102
|
+
async translate(keys, sourceFlat, pairConfig, options) {
|
|
103
|
+
const apiKey = this._resolveApiKey(options);
|
|
104
|
+
const region = this._resolveRegion(options);
|
|
105
|
+
const endpoint = this._resolveEndpoint(options);
|
|
106
|
+
|
|
107
|
+
if (!apiKey) {
|
|
108
|
+
output.warn('Microsoft Translator: no API key — skipping.');
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const targetLocale = pairConfig.target;
|
|
113
|
+
const sourceLocale = pairConfig.source || 'en';
|
|
114
|
+
const allTranslated = {};
|
|
115
|
+
|
|
116
|
+
// Microsoft supports up to 100 segments per request
|
|
117
|
+
const maxSegments = pairConfig.batchSize || options.batchSize || 100;
|
|
118
|
+
|
|
119
|
+
const batchChunks = [];
|
|
120
|
+
for (let i = 0; i < keys.length; i += maxSegments) {
|
|
121
|
+
batchChunks.push(keys.slice(i, i + maxSegments));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await pMap(batchChunks, async (chunk, idx) => {
|
|
125
|
+
const orderedKeys = [];
|
|
126
|
+
const sourceTexts = [];
|
|
127
|
+
|
|
128
|
+
for (const key of chunk) {
|
|
129
|
+
const val = sourceFlat[key];
|
|
130
|
+
if (val && typeof val === 'string') {
|
|
131
|
+
orderedKeys.push(key);
|
|
132
|
+
sourceTexts.push(val);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (sourceTexts.length === 0) return;
|
|
137
|
+
|
|
138
|
+
const result = await this._translateBatchWithRetry({
|
|
139
|
+
apiKey,
|
|
140
|
+
region,
|
|
141
|
+
endpoint,
|
|
142
|
+
orderedKeys,
|
|
143
|
+
sourceTexts,
|
|
144
|
+
sourceLocale,
|
|
145
|
+
targetLocale,
|
|
146
|
+
batchNum: idx + 1,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (result) {
|
|
150
|
+
Object.assign(allTranslated, result);
|
|
151
|
+
}
|
|
152
|
+
}, { concurrency: DEFAULT_METHOD_CONCURRENCY });
|
|
153
|
+
|
|
154
|
+
return Object.keys(allTranslated).length > 0 ? allTranslated : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Translate freeform Markdown content via Microsoft Translator API.
|
|
159
|
+
*
|
|
160
|
+
* Same protect/restore approach — ⟦PROTECTED_N⟧ placeholders shield
|
|
161
|
+
* code blocks and shortcodes from the translation engine.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} prompt - Complete translation prompt from buildContentPrompt()
|
|
164
|
+
* @param {import('../types.js').PairConfig} pairConfig - Pair config
|
|
165
|
+
* @param {object} options - { apiKey }
|
|
166
|
+
* @returns {string|null} Translated text, or null on failure
|
|
167
|
+
*/
|
|
168
|
+
async translateContent(prompt, pairConfig, options) {
|
|
169
|
+
const apiKey = this._resolveApiKey(options);
|
|
170
|
+
const region = this._resolveRegion(options);
|
|
171
|
+
|
|
172
|
+
if (!apiKey) {
|
|
173
|
+
output.warn('Microsoft Translator: no API key — skipping.');
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Extract the Markdown body from the translation prompt
|
|
178
|
+
const bodyText = extractContentBody(prompt);
|
|
179
|
+
if (!bodyText.trim()) return null;
|
|
180
|
+
|
|
181
|
+
const targetLocale = pairConfig.target;
|
|
182
|
+
const sourceLocale = pairConfig.source || 'en';
|
|
183
|
+
|
|
184
|
+
const endpoint = this._resolveEndpoint(options);
|
|
185
|
+
const url = this._buildUrl(endpoint, sourceLocale, targetLocale);
|
|
186
|
+
const headers = {
|
|
187
|
+
'Content-Type': 'application/json; charset=UTF-8',
|
|
188
|
+
'Ocp-Apim-Subscription-Key': apiKey,
|
|
189
|
+
};
|
|
190
|
+
if (region) {
|
|
191
|
+
headers['Ocp-Apim-Subscription-Region'] = region;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const response = await fetchWithRetry(url, {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers,
|
|
197
|
+
body: JSON.stringify([{ Text: bodyText }]),
|
|
198
|
+
}, {
|
|
199
|
+
label: 'Microsoft content',
|
|
200
|
+
timeoutMs: MICROSOFT_REQUEST_TIMEOUT_MS * 2,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (!response) return null;
|
|
204
|
+
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
const errorBody = await response.text();
|
|
207
|
+
output.error(`Microsoft content: ${response.status} — ${errorBody}`);
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const json = await response.json();
|
|
212
|
+
if (!json || json.length === 0 || !json[0].translations?.[0]?.text) {
|
|
213
|
+
output.error('Microsoft content: empty response');
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return json[0].translations[0].text;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
estimateCost(keyCount) {
|
|
221
|
+
return estimateProviderCost('microsoft-translator', keyCount);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
checkReadiness(context = {}) {
|
|
225
|
+
// Resolve through the same SSOT the loader uses so readiness can never
|
|
226
|
+
// pass under a name translate() won't read (or vice versa).
|
|
227
|
+
const { value } = resolveProviderEnv('microsoft-translator', context.cwd);
|
|
228
|
+
if (!value) {
|
|
229
|
+
return { ready: false, reason: `No Microsoft Translator key (${envNamesLabel('microsoft-translator')}).` };
|
|
230
|
+
}
|
|
231
|
+
return { ready: true };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getQualityTier() {
|
|
235
|
+
return 'standard';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
getProvenance() {
|
|
239
|
+
return {
|
|
240
|
+
resources: [
|
|
241
|
+
{
|
|
242
|
+
name: 'Microsoft Translator API',
|
|
243
|
+
license: 'Proprietary (Microsoft ToS)',
|
|
244
|
+
type: 'api',
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
commercialReady: true,
|
|
248
|
+
flags: [],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
getSetupHelp() {
|
|
253
|
+
const { value: apiKey } = resolveProviderEnv('microsoft-translator');
|
|
254
|
+
if (!apiKey) {
|
|
255
|
+
return [
|
|
256
|
+
'',
|
|
257
|
+
' ┌─ Missing API Key ─────────────────────────────────────────────┐',
|
|
258
|
+
' │ Microsoft Translator requires an Azure subscription key. │',
|
|
259
|
+
' │ │',
|
|
260
|
+
' │ 1. Create a Translator resource in Azure Portal │',
|
|
261
|
+
' │ 2. Run: export MICROSOFT_TRANSLATOR_API_KEY=... │',
|
|
262
|
+
' │ 3. Set region: export MICROSOFT_TRANSLATOR_REGION=eastus │',
|
|
263
|
+
' │ │',
|
|
264
|
+
' │ Docs: https://learn.microsoft.com/azure/cognitive-services/ │',
|
|
265
|
+
' │ translator/quickstart-text-rest-api │',
|
|
266
|
+
' └────────────────────────────────────────────────────────────────┘',
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
return this._apiFailureHelp('Azure Portal');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async _translateBatchWithRetry({
|
|
273
|
+
apiKey,
|
|
274
|
+
region,
|
|
275
|
+
endpoint,
|
|
276
|
+
orderedKeys,
|
|
277
|
+
sourceTexts,
|
|
278
|
+
sourceLocale,
|
|
279
|
+
targetLocale,
|
|
280
|
+
batchNum,
|
|
281
|
+
}) {
|
|
282
|
+
const url = this._buildUrl(endpoint, sourceLocale, targetLocale);
|
|
283
|
+
|
|
284
|
+
const headers = {
|
|
285
|
+
'Content-Type': 'application/json; charset=UTF-8',
|
|
286
|
+
'Ocp-Apim-Subscription-Key': apiKey,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
if (region) {
|
|
290
|
+
headers['Ocp-Apim-Subscription-Region'] = region;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const body = sourceTexts.map(text => ({ Text: text }));
|
|
294
|
+
|
|
295
|
+
const response = await fetchWithRetry(url, {
|
|
296
|
+
method: 'POST',
|
|
297
|
+
headers,
|
|
298
|
+
body: JSON.stringify(body),
|
|
299
|
+
}, {
|
|
300
|
+
label: `Microsoft Translator batch ${batchNum}`,
|
|
301
|
+
timeoutMs: MICROSOFT_REQUEST_TIMEOUT_MS,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (!response) return null;
|
|
305
|
+
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
const errorBody = await response.text();
|
|
308
|
+
output.error(`Microsoft Translator batch ${batchNum}: ${response.status} — ${errorBody}`);
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const json = await response.json();
|
|
313
|
+
|
|
314
|
+
if (!json || json.length !== orderedKeys.length) {
|
|
315
|
+
output.error(`Microsoft Translator batch ${batchNum}: Response length mismatch (expected ${orderedKeys.length}, got ${json?.length || 0})`);
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const result = {};
|
|
320
|
+
for (let i = 0; i < orderedKeys.length; i++) {
|
|
321
|
+
result[orderedKeys[i]] = json[i].translations[0].text;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const charCount = sourceTexts.reduce((sum, t) => sum + t.length, 0);
|
|
325
|
+
output.progress(` ✓ Microsoft batch ${batchNum} (${orderedKeys.length} keys, ${charCount} chars)`);
|
|
326
|
+
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export { MicrosoftTranslatorMethod };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Translation Method — direct OpenAI Chat Completions API.
|
|
3
|
+
*
|
|
4
|
+
* Extends DirectLLMMethod to provide OpenAI-specific:
|
|
5
|
+
* - API endpoint (https://api.openai.com/v1/chat/completions)
|
|
6
|
+
* - Auth header format (Bearer token)
|
|
7
|
+
* - Request body (messages array, response_format for JSON mode)
|
|
8
|
+
* - Response parsing (choices[0].message.content)
|
|
9
|
+
* - Model listing via GET /v1/models
|
|
10
|
+
* - Pricing (gpt-4o, gpt-4o-mini)
|
|
11
|
+
*
|
|
12
|
+
* All shared logic (translate, translateContent, coaching, retry, validation)
|
|
13
|
+
* lives in DirectLLMMethod.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { DirectLLMMethod } from './direct-llm.js';
|
|
17
|
+
import { fetchAvailableModels } from '../models.js';
|
|
18
|
+
import { estimateLlmCost } from './provider-pricing.js';
|
|
19
|
+
|
|
20
|
+
// The default model, named ONCE — it was previously written twice (here and
|
|
21
|
+
// as an inline fallback in estimateCost()) and the two could drift.
|
|
22
|
+
const DEFAULT_MODEL = 'gpt-4o';
|
|
23
|
+
|
|
24
|
+
class OpenAIMethod extends DirectLLMMethod {
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
super(options);
|
|
27
|
+
this.name = 'openai';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Provider identity ────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
_getApiKeyEnvVar() { return 'OPENAI_API_KEY'; }
|
|
33
|
+
_getApiKeyOptionsKey() { return 'openaiApiKey'; }
|
|
34
|
+
_getDefaultModel() { return DEFAULT_MODEL; }
|
|
35
|
+
_getProviderLabel() { return 'OpenAI'; }
|
|
36
|
+
_getDefaultApiBase() { return 'https://api.openai.com/v1'; }
|
|
37
|
+
|
|
38
|
+
// ── API request/response shape ───────────────────────────────────
|
|
39
|
+
|
|
40
|
+
_buildApiRequest({ prompt, systemMessage, apiKey, model, temperature, isJsonMode }) {
|
|
41
|
+
const messages = systemMessage
|
|
42
|
+
? [{ role: 'system', content: systemMessage }, { role: 'user', content: prompt }]
|
|
43
|
+
: [{ role: 'user', content: prompt }];
|
|
44
|
+
|
|
45
|
+
const body = { model, messages, temperature };
|
|
46
|
+
if (isJsonMode) {
|
|
47
|
+
body.response_format = { type: 'json_object' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const base = this._resolveApiBase() || 'https://api.openai.com/v1';
|
|
51
|
+
return {
|
|
52
|
+
url: `${base}/chat/completions`,
|
|
53
|
+
headers: {
|
|
54
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
},
|
|
57
|
+
body,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_extractResponseText(json) {
|
|
62
|
+
return json.choices?.[0]?.message?.content || null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Runtime model listing ────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
async _fetchModels(apiKey) {
|
|
68
|
+
// Delegate to the shared models.js module — single source of truth for
|
|
69
|
+
// model listing used by init wizard, `champollion models`, and validation.
|
|
70
|
+
return fetchAvailableModels('openai', apiKey);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Model-aware quality tier ─────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
_getModelTier(model) {
|
|
76
|
+
if (model.includes('mini')) return 'budget';
|
|
77
|
+
if (model.startsWith('o1') || model.startsWith('o3') || model.startsWith('o4')) return 'premium';
|
|
78
|
+
return 'standard'; // gpt-4o and similar
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Pricing ──────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
async estimateCost(keyCount, pairConfig = {}) {
|
|
84
|
+
return estimateLlmCost('openai', pairConfig.model || DEFAULT_MODEL, keyCount);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Provenance ───────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
checkReadiness(context) {
|
|
90
|
+
// Resolve through the same chain translate() uses (options → env →
|
|
91
|
+
// .env.local/.env in cwd) so readiness can never fail for a key the
|
|
92
|
+
// loader *would* read — e.g. OPENAI_API_KEY set only in .env.local.
|
|
93
|
+
if (!this._resolveApiKey(context || {})) {
|
|
94
|
+
return { ready: false, reason: 'No OpenAI API key (OPENAI_API_KEY).' };
|
|
95
|
+
}
|
|
96
|
+
return { ready: true };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getProvenance() {
|
|
100
|
+
return {
|
|
101
|
+
resources: [
|
|
102
|
+
{
|
|
103
|
+
name: 'OpenAI Developer API',
|
|
104
|
+
license: 'Proprietary (OpenAI ToS)',
|
|
105
|
+
type: 'api',
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
commercialReady: true,
|
|
109
|
+
flags: [],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getSetupHelp() {
|
|
114
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
115
|
+
if (!apiKey) {
|
|
116
|
+
return [
|
|
117
|
+
'',
|
|
118
|
+
' ┌─ Missing API Key ─────────────────────────────────────────────┐',
|
|
119
|
+
' │ The OpenAI method requires an OpenAI API key. │',
|
|
120
|
+
' │ │',
|
|
121
|
+
' │ 1. Sign up at https://platform.openai.com │',
|
|
122
|
+
' │ 2. Run: export OPENAI_API_KEY=sk-... │',
|
|
123
|
+
' │ 3. Or add to .env.local: OPENAI_API_KEY=sk-... │',
|
|
124
|
+
' └────────────────────────────────────────────────────────────────┘',
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
return this._apiFailureHelp('OpenAI dashboard');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { OpenAIMethod };
|