@saidksi/localizer-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/dist/ai/anthropic.d.ts +17 -0
- package/dist/ai/anthropic.d.ts.map +1 -0
- package/dist/ai/anthropic.js +58 -0
- package/dist/ai/anthropic.js.map +1 -0
- package/dist/ai/dedup.d.ts +19 -0
- package/dist/ai/dedup.d.ts.map +1 -0
- package/dist/ai/dedup.js +119 -0
- package/dist/ai/dedup.js.map +1 -0
- package/dist/ai/index.d.ts +65 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +464 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/openai.d.ts +11 -0
- package/dist/ai/openai.d.ts.map +1 -0
- package/dist/ai/openai.js +62 -0
- package/dist/ai/openai.js.map +1 -0
- package/dist/ai/prompts.d.ts +20 -0
- package/dist/ai/prompts.d.ts.map +1 -0
- package/dist/ai/prompts.js +151 -0
- package/dist/ai/prompts.js.map +1 -0
- package/dist/cache/index.d.ts +69 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +129 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/rewriter/index.d.ts +31 -0
- package/dist/rewriter/index.d.ts.map +1 -0
- package/dist/rewriter/index.js +128 -0
- package/dist/rewriter/index.js.map +1 -0
- package/dist/rewriter/transforms.d.ts +38 -0
- package/dist/rewriter/transforms.d.ts.map +1 -0
- package/dist/rewriter/transforms.js +189 -0
- package/dist/rewriter/transforms.js.map +1 -0
- package/dist/rewriter/ts-morph.d.ts +19 -0
- package/dist/rewriter/ts-morph.d.ts.map +1 -0
- package/dist/rewriter/ts-morph.js +121 -0
- package/dist/rewriter/ts-morph.js.map +1 -0
- package/dist/scanner/babel.d.ts +3 -0
- package/dist/scanner/babel.d.ts.map +1 -0
- package/dist/scanner/babel.js +504 -0
- package/dist/scanner/babel.js.map +1 -0
- package/dist/scanner/filters.d.ts +38 -0
- package/dist/scanner/filters.d.ts.map +1 -0
- package/dist/scanner/filters.js +133 -0
- package/dist/scanner/filters.js.map +1 -0
- package/dist/scanner/index.d.ts +22 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +82 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/typescript.d.ts +3 -0
- package/dist/scanner/typescript.d.ts.map +1 -0
- package/dist/scanner/typescript.js +542 -0
- package/dist/scanner/typescript.js.map +1 -0
- package/dist/types.d.ts +205 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/validator/index.d.ts +65 -0
- package/dist/validator/index.d.ts.map +1 -0
- package/dist/validator/index.js +237 -0
- package/dist/validator/index.js.map +1 -0
- package/package.json +65 -0
package/dist/ai/index.js
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
2
|
+
import { resolve, basename, extname, join } from "path";
|
|
3
|
+
import { deduplicateResults, buildAIRequests, applyResolvedKeys } from "./dedup.js";
|
|
4
|
+
import { callAnthropic, estimateCost } from "./anthropic.js";
|
|
5
|
+
import { callOpenAI } from "./openai.js";
|
|
6
|
+
import { flattenKeys } from "../validator/index.js";
|
|
7
|
+
// ─── Key conflict resolution (flat key vs nested namespace) ──────────────────
|
|
8
|
+
/**
|
|
9
|
+
* Detect and fix key namespace conflicts.
|
|
10
|
+
*
|
|
11
|
+
* When a flat key (e.g. "dashboard.admin_panel") is also a prefix of another
|
|
12
|
+
* key (e.g. "dashboard.admin_panel.total_users"), JSON cannot represent it:
|
|
13
|
+
* a value cannot be both a string leaf and an object node at the same path.
|
|
14
|
+
*
|
|
15
|
+
* Fix: append ".title" to the flat key so it becomes a leaf under the object.
|
|
16
|
+
* "dashboard.admin_panel" → "dashboard.admin_panel.title"
|
|
17
|
+
*/
|
|
18
|
+
function resolveKeyConflicts(responses) {
|
|
19
|
+
const allKeys = Array.from(responses.values()).map((r) => r.key);
|
|
20
|
+
for (const [value, response] of responses) {
|
|
21
|
+
const key = response.key;
|
|
22
|
+
const hasChildren = allKeys.some((k) => k !== key && k.startsWith(`${key}.`));
|
|
23
|
+
if (hasChildren) {
|
|
24
|
+
responses.set(value, {
|
|
25
|
+
key: `${key}.title`,
|
|
26
|
+
translations: response.translations,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// ─── Key normalization (fix inconsistent namespaces for siblings) ───────────────
|
|
32
|
+
/**
|
|
33
|
+
* Detect and fix key namespace inconsistencies for sibling strings.
|
|
34
|
+
* If "Total views" and "Total clicks" get different namespace roots
|
|
35
|
+
* (e.g., "dashboard.statistics" vs "dashboard.metrics"), normalize them.
|
|
36
|
+
*
|
|
37
|
+
* Strategy:
|
|
38
|
+
* 1. Group keys by their contextKey (file + component)
|
|
39
|
+
* 2. For each group, find the most common namespace prefix
|
|
40
|
+
* 3. Apply it to all siblings with conflicting prefixes
|
|
41
|
+
*/
|
|
42
|
+
function normalizeConsistentKeys(requests, responses) {
|
|
43
|
+
// Group requests by contextKey
|
|
44
|
+
const byContext = new Map();
|
|
45
|
+
for (const req of requests) {
|
|
46
|
+
const key = req.contextKey || `${req.file}:unknown`;
|
|
47
|
+
if (!byContext.has(key))
|
|
48
|
+
byContext.set(key, []);
|
|
49
|
+
byContext.get(key).push(req);
|
|
50
|
+
}
|
|
51
|
+
// For each context group, check for namespace inconsistencies
|
|
52
|
+
for (const [, contextRequests] of byContext) {
|
|
53
|
+
if (contextRequests.length < 2)
|
|
54
|
+
continue; // No siblings to check
|
|
55
|
+
// Collect all keys and their namespace roots
|
|
56
|
+
const keysByRoot = new Map();
|
|
57
|
+
for (const req of contextRequests) {
|
|
58
|
+
const response = responses.get(req.value);
|
|
59
|
+
if (!response)
|
|
60
|
+
continue;
|
|
61
|
+
const key = response.key;
|
|
62
|
+
const parts = key.split(".");
|
|
63
|
+
const root = parts.length > 1 ? parts[0] : key; // first segment = root
|
|
64
|
+
if (!keysByRoot.has(root))
|
|
65
|
+
keysByRoot.set(root, []);
|
|
66
|
+
keysByRoot.get(root).push({ req, key });
|
|
67
|
+
}
|
|
68
|
+
// If there are multiple roots (inconsistency), pick the most common one
|
|
69
|
+
if (keysByRoot.size > 1) {
|
|
70
|
+
const roots = Array.from(keysByRoot.entries());
|
|
71
|
+
const mostCommon = roots.sort((a, b) => b[1].length - a[1].length)[0];
|
|
72
|
+
if (!mostCommon)
|
|
73
|
+
continue;
|
|
74
|
+
const [dominantRoot, dominantEntries] = mostCommon;
|
|
75
|
+
// Reassign keys with other roots to use the dominant root
|
|
76
|
+
for (const [otherRoot, entries] of roots) {
|
|
77
|
+
if (otherRoot === dominantRoot)
|
|
78
|
+
continue;
|
|
79
|
+
for (const { req, key } of entries) {
|
|
80
|
+
const keyWithoutRoot = key.substring(otherRoot.length + 1); // remove root + dot
|
|
81
|
+
const newKey = `${dominantRoot}.${keyWithoutRoot}`;
|
|
82
|
+
// Update the response
|
|
83
|
+
const oldResponse = responses.get(req.value);
|
|
84
|
+
if (oldResponse) {
|
|
85
|
+
responses.set(req.value, {
|
|
86
|
+
key: newKey,
|
|
87
|
+
translations: oldResponse.translations,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ─── Messages JSON helpers ────────────────────────────────────────────────────
|
|
96
|
+
/** "src/pages/Login.tsx" → "login" */
|
|
97
|
+
function getPageName(filePath) {
|
|
98
|
+
return basename(filePath, extname(filePath)).toLowerCase();
|
|
99
|
+
}
|
|
100
|
+
function isPlainObject(v) {
|
|
101
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Deep merge `source` into `target`.
|
|
105
|
+
* When `overwrite` is false, existing leaf values in target are preserved.
|
|
106
|
+
*/
|
|
107
|
+
function deepMerge(target, source, overwrite) {
|
|
108
|
+
const result = { ...target };
|
|
109
|
+
for (const [key, value] of Object.entries(source)) {
|
|
110
|
+
if (key in result) {
|
|
111
|
+
if (isPlainObject(result[key]) && isPlainObject(value)) {
|
|
112
|
+
result[key] = deepMerge(result[key], value, overwrite);
|
|
113
|
+
}
|
|
114
|
+
else if (overwrite) {
|
|
115
|
+
result[key] = value;
|
|
116
|
+
}
|
|
117
|
+
// If overwrite=false and key exists as a leaf → keep existing
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
result[key] = value;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Expand a dot-notation key + value into a nested object.
|
|
127
|
+
* "auth.sign_in_button", "Sign in" → { auth: { sign_in_button: "Sign in" } }
|
|
128
|
+
* snake_case keys are stored flat.
|
|
129
|
+
*/
|
|
130
|
+
function expandKey(key, value) {
|
|
131
|
+
const parts = key.split(".");
|
|
132
|
+
if (parts.length === 1)
|
|
133
|
+
return { [key]: value };
|
|
134
|
+
const result = {};
|
|
135
|
+
let current = result;
|
|
136
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
137
|
+
const part = parts[i];
|
|
138
|
+
current[part] = {};
|
|
139
|
+
current = current[part];
|
|
140
|
+
}
|
|
141
|
+
current[parts[parts.length - 1]] = value;
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
/** Read existing messages JSON or return empty object if file doesn't exist. */
|
|
145
|
+
async function readMessagesFile(filePath) {
|
|
146
|
+
try {
|
|
147
|
+
const content = await readFile(filePath, "utf-8");
|
|
148
|
+
return JSON.parse(content);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Merge a set of (key → translation) pairs into a messages JSON file.
|
|
156
|
+
* Creates the file and parent directories if they don't exist.
|
|
157
|
+
*/
|
|
158
|
+
async function mergeIntoMessagesFile(filePath, entries, overwrite) {
|
|
159
|
+
const existing = await readMessagesFile(filePath);
|
|
160
|
+
let merged = existing;
|
|
161
|
+
for (const { key, translation } of entries) {
|
|
162
|
+
const expanded = expandKey(key, translation);
|
|
163
|
+
merged = deepMerge(merged, expanded, overwrite);
|
|
164
|
+
}
|
|
165
|
+
await mkdir(resolve(filePath, ".."), { recursive: true });
|
|
166
|
+
await writeFile(filePath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
167
|
+
}
|
|
168
|
+
// ─── Write translations to disk ──────────────────────────────────────────────
|
|
169
|
+
/**
|
|
170
|
+
* Given resolved scan results, write all translations to the messages directory.
|
|
171
|
+
* Groups by source file → writes to messages/{lang}/{pageName}.json.
|
|
172
|
+
*
|
|
173
|
+
* Returns the list of file paths written.
|
|
174
|
+
*/
|
|
175
|
+
async function writeTranslations(results, responses, config, options) {
|
|
176
|
+
// Group resolved results by source file
|
|
177
|
+
const byFile = new Map();
|
|
178
|
+
for (const result of results) {
|
|
179
|
+
if (!result.resolvedKey)
|
|
180
|
+
continue;
|
|
181
|
+
const existing = byFile.get(result.file);
|
|
182
|
+
if (existing) {
|
|
183
|
+
existing.push(result);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
byFile.set(result.file, [result]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const writtenPaths = [];
|
|
190
|
+
for (const [sourceFile, fileResults] of byFile) {
|
|
191
|
+
const pageName = getPageName(sourceFile);
|
|
192
|
+
const allLanguages = [config.defaultLanguage, ...config.languages];
|
|
193
|
+
// Collect unique keys for this file (deduplicated by key)
|
|
194
|
+
const seen = new Set();
|
|
195
|
+
const entries = [];
|
|
196
|
+
for (const result of fileResults) {
|
|
197
|
+
const key = result.resolvedKey;
|
|
198
|
+
if (seen.has(key))
|
|
199
|
+
continue;
|
|
200
|
+
seen.add(key);
|
|
201
|
+
const response = responses.get(result.value);
|
|
202
|
+
if (response)
|
|
203
|
+
entries.push({ key, response });
|
|
204
|
+
}
|
|
205
|
+
for (const lang of allLanguages) {
|
|
206
|
+
const messagesPath = join(resolve(config.messagesDir), lang, `${pageName}.json`);
|
|
207
|
+
// Build (key, translation) pairs for this language
|
|
208
|
+
const langEntries = entries.map(({ key, response }) => ({
|
|
209
|
+
key,
|
|
210
|
+
// Default language gets the original string value
|
|
211
|
+
translation: lang === config.defaultLanguage
|
|
212
|
+
? (response.translations[lang] ?? fileResults.find((r) => r.resolvedKey === key)?.value ?? "")
|
|
213
|
+
: (response.translations[lang] ?? ""),
|
|
214
|
+
}));
|
|
215
|
+
if (!options.dryRun) {
|
|
216
|
+
await mergeIntoMessagesFile(messagesPath, langEntries, options.overwrite);
|
|
217
|
+
}
|
|
218
|
+
writtenPaths.push(messagesPath);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return writtenPaths;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Translate a list of scan results using the configured AI provider.
|
|
225
|
+
*
|
|
226
|
+
* Steps:
|
|
227
|
+
* 1. Deduplicate by string value
|
|
228
|
+
* 2. Build AI requests (one per unique string)
|
|
229
|
+
* 3. Call Anthropic or OpenAI
|
|
230
|
+
* 4. Assign resolvedKey back to all results
|
|
231
|
+
* 5. Write to messages/{lang}/{pageName}.json (unless dryRun)
|
|
232
|
+
*/
|
|
233
|
+
export async function translateStrings(scanResults, config, apiKey, options = {}) {
|
|
234
|
+
const { dryRun = false, overwrite = config.overwriteExisting } = options;
|
|
235
|
+
// Filter out already-translated strings
|
|
236
|
+
const untranslated = scanResults.filter((r) => !r.alreadyTranslated);
|
|
237
|
+
if (untranslated.length === 0) {
|
|
238
|
+
return {
|
|
239
|
+
results: scanResults,
|
|
240
|
+
aiCostUsd: 0,
|
|
241
|
+
messagesWritten: [],
|
|
242
|
+
uniqueStrings: 0,
|
|
243
|
+
aiCalls: 0,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
// 1. Deduplicate
|
|
247
|
+
const groups = deduplicateResults(untranslated);
|
|
248
|
+
const uniqueStrings = groups.size;
|
|
249
|
+
// 2. Build requests (pass allResults to find sibling strings for consistent naming)
|
|
250
|
+
const requests = buildAIRequests(groups, config, untranslated);
|
|
251
|
+
// 3. Call AI provider
|
|
252
|
+
let aiResponses;
|
|
253
|
+
let totalCostUsd;
|
|
254
|
+
if (config.aiProvider === "anthropic") {
|
|
255
|
+
const result = await callAnthropic(requests, config.aiModel, apiKey);
|
|
256
|
+
aiResponses = result.responses;
|
|
257
|
+
totalCostUsd = result.totalCostUsd;
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const result = await callOpenAI(requests, config.aiModel, apiKey);
|
|
261
|
+
aiResponses = result.responses;
|
|
262
|
+
totalCostUsd = result.totalCostUsd;
|
|
263
|
+
}
|
|
264
|
+
// 3.5. Normalize inconsistent key namespaces for sibling strings
|
|
265
|
+
// CRITICAL: normalizeConsistentKeys must run BEFORE resolveKeyConflicts.
|
|
266
|
+
// normalizeConsistentKeys may create parent keys that would conflict with existing child keys,
|
|
267
|
+
// so resolveKeyConflicts must then fix those conflicts.
|
|
268
|
+
normalizeConsistentKeys(requests, aiResponses);
|
|
269
|
+
// 3.6. Fix flat-key vs nested-namespace conflicts (e.g. "admin_panel" + "admin_panel.total_users")
|
|
270
|
+
resolveKeyConflicts(aiResponses);
|
|
271
|
+
// 4. Assign resolved keys back to all results (including the already-translated ones)
|
|
272
|
+
const valueToKey = new Map();
|
|
273
|
+
for (const [value, response] of aiResponses) {
|
|
274
|
+
valueToKey.set(value, response.key);
|
|
275
|
+
}
|
|
276
|
+
const updatedResults = applyResolvedKeys(scanResults, valueToKey);
|
|
277
|
+
// 5. Write translations to disk
|
|
278
|
+
const messagesWritten = await writeTranslations(updatedResults, aiResponses, config, { dryRun, overwrite });
|
|
279
|
+
return {
|
|
280
|
+
results: updatedResults,
|
|
281
|
+
aiCostUsd: totalCostUsd,
|
|
282
|
+
messagesWritten: dryRun ? [] : messagesWritten,
|
|
283
|
+
uniqueStrings,
|
|
284
|
+
aiCalls: aiResponses.size,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Translate a list of existing (key, value, pageName) entries into target languages.
|
|
289
|
+
* Used by `localize translate --from-existing` and `localize add-lang`.
|
|
290
|
+
*
|
|
291
|
+
* Skips entries that already have translations in target languages
|
|
292
|
+
* unless `overwrite` is true.
|
|
293
|
+
*/
|
|
294
|
+
export async function translateExistingKeys(entries, config, apiKey, options = {}) {
|
|
295
|
+
const { dryRun = false, overwrite = config.overwriteExisting, langs = config.languages, } = options;
|
|
296
|
+
if (entries.length === 0) {
|
|
297
|
+
return { translated: 0, aiCostUsd: 0, messagesWritten: [], aiCalls: 0 };
|
|
298
|
+
}
|
|
299
|
+
// ── Pre-filter when not overwriting (--missing-only) ──────────────────────
|
|
300
|
+
// Read each target language's existing keys and discard entries that are
|
|
301
|
+
// already present in ALL target languages. This avoids wasting AI calls on
|
|
302
|
+
// keys that are fully covered — only keys missing from at least one language
|
|
303
|
+
// are sent to the AI provider.
|
|
304
|
+
let entriesToTranslate = entries;
|
|
305
|
+
if (!overwrite) {
|
|
306
|
+
// Build a map of lang → Set<"pageName.key"> for fast lookup
|
|
307
|
+
const existingByLang = new Map();
|
|
308
|
+
const pages = new Set(entries.map((e) => e.pageName));
|
|
309
|
+
await Promise.all(langs.map(async (lang) => {
|
|
310
|
+
const keys = new Set();
|
|
311
|
+
for (const page of pages) {
|
|
312
|
+
const filePath = join(resolve(config.messagesDir), lang, `${page}.json`);
|
|
313
|
+
try {
|
|
314
|
+
const raw = await readFile(filePath, "utf-8");
|
|
315
|
+
const parsed = JSON.parse(raw);
|
|
316
|
+
for (const k of flattenKeys(parsed))
|
|
317
|
+
keys.add(`${page}.${k}`);
|
|
318
|
+
}
|
|
319
|
+
catch { /* file not yet created — all keys are missing */ }
|
|
320
|
+
}
|
|
321
|
+
existingByLang.set(lang, keys);
|
|
322
|
+
}));
|
|
323
|
+
entriesToTranslate = entries.filter((entry) =>
|
|
324
|
+
// Keep if missing from at least one target language
|
|
325
|
+
langs.some((lang) => !existingByLang.get(lang)?.has(`${entry.pageName}.${entry.key}`)));
|
|
326
|
+
if (entriesToTranslate.length === 0) {
|
|
327
|
+
return { translated: 0, aiCostUsd: 0, messagesWritten: [], aiCalls: 0 };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Dedup by value — same string value gets one AI call
|
|
331
|
+
const uniqueValues = [...new Set(entriesToTranslate.map((e) => e.value))];
|
|
332
|
+
// Build one request per unique value
|
|
333
|
+
const { buildTranslationOnlyPrompt, parseTranslationOnlyResponse } = await import("./prompts.js");
|
|
334
|
+
const limit = (await import("p-limit")).default;
|
|
335
|
+
const pLimit = limit(5);
|
|
336
|
+
const valueToTranslations = new Map();
|
|
337
|
+
let totalCostUsd = 0;
|
|
338
|
+
await Promise.all(uniqueValues.map((value) => pLimit(async () => {
|
|
339
|
+
// Build per-string glossary
|
|
340
|
+
const glossary = {};
|
|
341
|
+
for (const [lang, terms] of Object.entries(config.glossary)) {
|
|
342
|
+
if (terms[value])
|
|
343
|
+
glossary[lang] = terms[value];
|
|
344
|
+
}
|
|
345
|
+
const prompt = buildTranslationOnlyPrompt(value, langs, glossary);
|
|
346
|
+
try {
|
|
347
|
+
if (config.aiProvider === "anthropic") {
|
|
348
|
+
const { default: Anthropic } = await import("@anthropic-ai/sdk");
|
|
349
|
+
const client = new Anthropic({ apiKey });
|
|
350
|
+
const msg = await client.messages.create({
|
|
351
|
+
model: config.aiModel,
|
|
352
|
+
max_tokens: 512,
|
|
353
|
+
messages: [{ role: "user", content: prompt }],
|
|
354
|
+
});
|
|
355
|
+
const raw = msg.content[0]?.type === "text" ? msg.content[0].text : "";
|
|
356
|
+
const translations = parseTranslationOnlyResponse(raw);
|
|
357
|
+
valueToTranslations.set(value, translations);
|
|
358
|
+
totalCostUsd += estimateCost(config.aiModel, msg.usage.input_tokens, msg.usage.output_tokens);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
const { default: OpenAI } = await import("openai");
|
|
362
|
+
const client = new OpenAI({ apiKey });
|
|
363
|
+
const res = await client.chat.completions.create({
|
|
364
|
+
model: config.aiModel,
|
|
365
|
+
messages: [{ role: "user", content: prompt }],
|
|
366
|
+
max_tokens: 512,
|
|
367
|
+
response_format: { type: "json_object" },
|
|
368
|
+
});
|
|
369
|
+
const raw = res.choices[0]?.message.content ?? "";
|
|
370
|
+
const translations = parseTranslationOnlyResponse(raw);
|
|
371
|
+
valueToTranslations.set(value, translations);
|
|
372
|
+
if (res.usage) {
|
|
373
|
+
totalCostUsd +=
|
|
374
|
+
(res.usage.prompt_tokens * 5 +
|
|
375
|
+
res.usage.completion_tokens * 15) /
|
|
376
|
+
1_000_000;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
console.error(`[localize] Translation failed for "${value}": ${String(err)}`);
|
|
382
|
+
}
|
|
383
|
+
})));
|
|
384
|
+
if (dryRun) {
|
|
385
|
+
return {
|
|
386
|
+
translated: valueToTranslations.size,
|
|
387
|
+
aiCostUsd: totalCostUsd,
|
|
388
|
+
messagesWritten: [],
|
|
389
|
+
aiCalls: uniqueValues.length,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
// Write translations grouped by pageName (only the filtered entries)
|
|
393
|
+
const writtenPaths = [];
|
|
394
|
+
const byPage = new Map();
|
|
395
|
+
for (const entry of entriesToTranslate) {
|
|
396
|
+
const existing = byPage.get(entry.pageName) ?? [];
|
|
397
|
+
existing.push(entry);
|
|
398
|
+
byPage.set(entry.pageName, existing);
|
|
399
|
+
}
|
|
400
|
+
for (const [pageName, pageEntries] of byPage) {
|
|
401
|
+
for (const lang of langs) {
|
|
402
|
+
const filePath = join(resolve(config.messagesDir), lang, `${pageName}.json`);
|
|
403
|
+
let existing = {};
|
|
404
|
+
try {
|
|
405
|
+
existing = JSON.parse(await readFile(filePath, "utf-8"));
|
|
406
|
+
}
|
|
407
|
+
catch { /* new file */ }
|
|
408
|
+
let merged = existing;
|
|
409
|
+
for (const { key, value } of pageEntries) {
|
|
410
|
+
const translations = valueToTranslations.get(value);
|
|
411
|
+
const translation = translations?.[lang];
|
|
412
|
+
if (!translation)
|
|
413
|
+
continue;
|
|
414
|
+
const expanded = expandKey(key, translation);
|
|
415
|
+
merged = deepMerge(merged, expanded, overwrite);
|
|
416
|
+
}
|
|
417
|
+
await mkdir(resolve(filePath, ".."), { recursive: true });
|
|
418
|
+
await writeFile(filePath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
419
|
+
writtenPaths.push(filePath);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
translated: entriesToTranslate.length,
|
|
424
|
+
aiCostUsd: totalCostUsd,
|
|
425
|
+
messagesWritten: writtenPaths,
|
|
426
|
+
aiCalls: uniqueValues.length,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
// ─── API key validation ───────────────────────────────────────────────────────
|
|
430
|
+
/**
|
|
431
|
+
* Send a minimal test request to verify an API key is valid.
|
|
432
|
+
* Used by `localize init` before saving the key to ~/.localize.
|
|
433
|
+
* Returns true if the key works, false otherwise.
|
|
434
|
+
*/
|
|
435
|
+
export async function validateApiKey(provider, model, apiKey) {
|
|
436
|
+
try {
|
|
437
|
+
if (provider === "anthropic") {
|
|
438
|
+
const { default: Anthropic } = await import("@anthropic-ai/sdk");
|
|
439
|
+
const client = new Anthropic({ apiKey });
|
|
440
|
+
await client.messages.create({
|
|
441
|
+
model,
|
|
442
|
+
max_tokens: 1,
|
|
443
|
+
messages: [{ role: "user", content: "test" }],
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
const { default: OpenAI } = await import("openai");
|
|
448
|
+
const client = new OpenAI({ apiKey });
|
|
449
|
+
await client.chat.completions.create({
|
|
450
|
+
model,
|
|
451
|
+
messages: [{ role: "user", content: "test" }],
|
|
452
|
+
max_tokens: 1,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Re-export for consumers that need lower-level access
|
|
462
|
+
export { deduplicateResults, buildAIRequests } from "./dedup.js";
|
|
463
|
+
export { buildTranslationPrompt, parseAIResponse } from "./prompts.js";
|
|
464
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ai/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AIRequest, AIResponse } from "../types.js";
|
|
2
|
+
export interface OpenAIResult {
|
|
3
|
+
responses: Map<string, AIResponse>;
|
|
4
|
+
totalCostUsd: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Call the OpenAI API for a batch of AI requests.
|
|
8
|
+
* Uses json_object response format for reliable JSON output.
|
|
9
|
+
*/
|
|
10
|
+
export declare function callOpenAI(requests: AIRequest[], model: string, apiKey: string): Promise<OpenAIResult>;
|
|
11
|
+
//# sourceMappingURL=openai.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../src/ai/openai.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA2BzD,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACnC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,SAAS,EAAE,EACrB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,YAAY,CAAC,CAwDvB"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import pLimit from "p-limit";
|
|
3
|
+
import { buildTranslationPrompt, parseAIResponse } from "./prompts.js";
|
|
4
|
+
const MAX_CONCURRENCY = 5;
|
|
5
|
+
// ─── Cost estimation ──────────────────────────────────────────────────────────
|
|
6
|
+
const OPENAI_RATES = {
|
|
7
|
+
"gpt-4o": { input: 5 / 1_000_000, output: 15 / 1_000_000 },
|
|
8
|
+
"gpt-4o-mini": { input: 0.15 / 1_000_000, output: 0.6 / 1_000_000 },
|
|
9
|
+
"gpt-4-turbo": { input: 10 / 1_000_000, output: 30 / 1_000_000 },
|
|
10
|
+
"gpt-3.5-turbo": { input: 0.5 / 1_000_000, output: 1.5 / 1_000_000 },
|
|
11
|
+
};
|
|
12
|
+
function estimateCost(model, promptTokens, completionTokens) {
|
|
13
|
+
// Match on prefix so "gpt-4o-mini-2024-07-18" maps to "gpt-4o-mini"
|
|
14
|
+
const key = Object.keys(OPENAI_RATES).find((k) => model.startsWith(k));
|
|
15
|
+
const rates = key ? OPENAI_RATES[key] : OPENAI_RATES["gpt-4o"];
|
|
16
|
+
return promptTokens * rates.input + completionTokens * rates.output;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Call the OpenAI API for a batch of AI requests.
|
|
20
|
+
* Uses json_object response format for reliable JSON output.
|
|
21
|
+
*/
|
|
22
|
+
export async function callOpenAI(requests, model, apiKey) {
|
|
23
|
+
const client = new OpenAI({ apiKey });
|
|
24
|
+
const limit = pLimit(MAX_CONCURRENCY);
|
|
25
|
+
const responses = new Map();
|
|
26
|
+
let totalCostUsd = 0;
|
|
27
|
+
await Promise.all(requests.map((request) => limit(async () => {
|
|
28
|
+
const prompt = buildTranslationPrompt(request);
|
|
29
|
+
let completion;
|
|
30
|
+
try {
|
|
31
|
+
completion = await client.chat.completions.create({
|
|
32
|
+
model,
|
|
33
|
+
messages: [{ role: "user", content: prompt }],
|
|
34
|
+
max_tokens: 1024,
|
|
35
|
+
response_format: { type: "json_object" },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error(`[localize] OpenAI call failed for "${request.value}": ${String(err)}`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const rawText = completion.choices[0]?.message.content ?? "";
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = parseAIResponse(rawText);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
console.error(`[localize] Failed to parse response for "${request.value}": ${String(err)}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const usage = completion.usage;
|
|
52
|
+
if (usage) {
|
|
53
|
+
totalCostUsd += estimateCost(model, usage.prompt_tokens, usage.completion_tokens);
|
|
54
|
+
}
|
|
55
|
+
responses.set(request.value, {
|
|
56
|
+
key: parsed.key,
|
|
57
|
+
translations: parsed.translations,
|
|
58
|
+
});
|
|
59
|
+
})));
|
|
60
|
+
return { responses, totalCostUsd };
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=openai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai.js","sourceRoot":"","sources":["../../src/ai/openai.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AIRequest } from "../types.js";
|
|
2
|
+
export declare function getLanguageName(code: string): string;
|
|
3
|
+
export declare function buildTranslationPrompt(request: AIRequest): string;
|
|
4
|
+
/**
|
|
5
|
+
* Simpler prompt used when keys already exist and we only need translations.
|
|
6
|
+
* Returns a flat { lang: translation } object — no key generation.
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildTranslationOnlyPrompt(value: string, targetLanguages: string[], glossary?: Record<string, string>): string;
|
|
9
|
+
/** Parse a flat translation-only response: { fr: "...", ar: "..." } */
|
|
10
|
+
export declare function parseTranslationOnlyResponse(text: string): Record<string, string>;
|
|
11
|
+
export interface ParsedAIResponse {
|
|
12
|
+
key: string;
|
|
13
|
+
translations: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Extract and validate the JSON object from an AI response string.
|
|
17
|
+
* Tolerates leading/trailing explanation text around the JSON.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseAIResponse(text: string): ParsedAIResponse;
|
|
20
|
+
//# sourceMappingURL=prompts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../src/ai/prompts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAkC7C,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAID,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,SAAS,GAAG,MAAM,CA0EjE;AAID;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EACb,eAAe,EAAE,MAAM,EAAE,EACzB,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GACpC,MAAM,CAyBR;AAED,uEAAuE;AACvE,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,GACX,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;AAID,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CA0B9D"}
|