glotfile 0.7.1 → 0.7.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/dist/server/cli.js +961 -512
- package/dist/server/server.js +515 -170
- package/dist/ui/assets/{index-Dx0VxxJh.js → index-C601vTV2.js} +5 -5
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
package/dist/server/cli.js
CHANGED
|
@@ -1972,6 +1972,9 @@ var init_export_run = __esm({
|
|
|
1972
1972
|
function supportsBatchTranslate(p) {
|
|
1973
1973
|
return typeof p.submitTranslationBatch === "function";
|
|
1974
1974
|
}
|
|
1975
|
+
function supportsBatchComplete(p) {
|
|
1976
|
+
return typeof p.submitCompletionBatch === "function";
|
|
1977
|
+
}
|
|
1975
1978
|
function buildSystemPrompt(hasPluralItems) {
|
|
1976
1979
|
const lines = [
|
|
1977
1980
|
"You are a professional software localization engine for a UI string catalog.",
|
|
@@ -2216,6 +2219,73 @@ var init_batch = __esm({
|
|
|
2216
2219
|
}
|
|
2217
2220
|
});
|
|
2218
2221
|
|
|
2222
|
+
// src/server/ai/pricing.ts
|
|
2223
|
+
function addUsage(into, add) {
|
|
2224
|
+
into.inputTokens += add.inputTokens;
|
|
2225
|
+
into.outputTokens += add.outputTokens;
|
|
2226
|
+
into.cacheCreationInputTokens += add.cacheCreationInputTokens;
|
|
2227
|
+
into.cacheReadInputTokens += add.cacheReadInputTokens;
|
|
2228
|
+
}
|
|
2229
|
+
function usageCostUsd(usage, ai, multiplier = 1) {
|
|
2230
|
+
if (!usage) return void 0;
|
|
2231
|
+
const pricing = resolvePricing(ai);
|
|
2232
|
+
return pricing ? estimateUsageCostUsd(usage, pricing, multiplier) : void 0;
|
|
2233
|
+
}
|
|
2234
|
+
function estimateUsageCostUsd(usage, pricing, multiplier = 1) {
|
|
2235
|
+
const inputCost = (usage.inputTokens + usage.cacheCreationInputTokens * CACHE_WRITE_MULTIPLIER + usage.cacheReadInputTokens * CACHE_READ_MULTIPLIER) * pricing.inputPerMTok;
|
|
2236
|
+
return (inputCost + usage.outputTokens * pricing.outputPerMTok) / 1e6 * multiplier;
|
|
2237
|
+
}
|
|
2238
|
+
function bareModelId(model) {
|
|
2239
|
+
let id = model.trim().toLowerCase();
|
|
2240
|
+
const slash = id.lastIndexOf("/");
|
|
2241
|
+
if (slash !== -1) id = id.slice(slash + 1);
|
|
2242
|
+
const anth = id.lastIndexOf("anthropic.");
|
|
2243
|
+
if (anth !== -1) id = id.slice(anth + "anthropic.".length);
|
|
2244
|
+
return id;
|
|
2245
|
+
}
|
|
2246
|
+
function resolvePricing(ai) {
|
|
2247
|
+
if (ai.inputPricePerMTok !== void 0 && ai.outputPricePerMTok !== void 0) {
|
|
2248
|
+
return { source: "profile", inputPerMTok: ai.inputPricePerMTok, outputPerMTok: ai.outputPricePerMTok };
|
|
2249
|
+
}
|
|
2250
|
+
if (FREE_PROVIDERS.has(ai.provider)) return { source: "builtin", inputPerMTok: 0, outputPerMTok: 0 };
|
|
2251
|
+
const id = bareModelId(ai.model);
|
|
2252
|
+
let best;
|
|
2253
|
+
for (const row of PRICE_TABLE) {
|
|
2254
|
+
if (id.startsWith(row[0]) && (!best || row[0].length > best[0].length)) best = row;
|
|
2255
|
+
}
|
|
2256
|
+
return best ? { source: "builtin", inputPerMTok: best[1], outputPerMTok: best[2] } : null;
|
|
2257
|
+
}
|
|
2258
|
+
var BATCH_PRICE_MULTIPLIER, CACHE_WRITE_MULTIPLIER, CACHE_READ_MULTIPLIER, PRICE_TABLE, FREE_PROVIDERS;
|
|
2259
|
+
var init_pricing = __esm({
|
|
2260
|
+
"src/server/ai/pricing.ts"() {
|
|
2261
|
+
"use strict";
|
|
2262
|
+
BATCH_PRICE_MULTIPLIER = 0.5;
|
|
2263
|
+
CACHE_WRITE_MULTIPLIER = 1.25;
|
|
2264
|
+
CACHE_READ_MULTIPLIER = 0.1;
|
|
2265
|
+
PRICE_TABLE = [
|
|
2266
|
+
["claude-fable-5", 10, 50],
|
|
2267
|
+
["claude-mythos-5", 10, 50],
|
|
2268
|
+
// Deprecated Opus 4.1 / 4.0 cost 3x the 4.5+ generation — they must outrank
|
|
2269
|
+
// the shorter "claude-opus-4" prefix (4-2025 covers the dated Opus 4 full IDs).
|
|
2270
|
+
["claude-opus-4-1", 15, 75],
|
|
2271
|
+
["claude-opus-4-0", 15, 75],
|
|
2272
|
+
["claude-opus-4-2025", 15, 75],
|
|
2273
|
+
["claude-opus-4", 5, 25],
|
|
2274
|
+
["claude-sonnet-4", 3, 15],
|
|
2275
|
+
["claude-haiku-4", 1, 5],
|
|
2276
|
+
["claude-3-5-haiku", 0.8, 4],
|
|
2277
|
+
["gpt-5.5-pro", 30, 180],
|
|
2278
|
+
["gpt-5.5", 5, 30],
|
|
2279
|
+
["gpt-5.4-pro", 30, 180],
|
|
2280
|
+
["gpt-5.4-mini", 0.75, 4.5],
|
|
2281
|
+
["gpt-5.4-nano", 0.2, 1.25],
|
|
2282
|
+
["gpt-5.4", 2.5, 15],
|
|
2283
|
+
["gpt-5.3-codex", 1.75, 14]
|
|
2284
|
+
];
|
|
2285
|
+
FREE_PROVIDERS = /* @__PURE__ */ new Set(["ollama", "claude-code"]);
|
|
2286
|
+
}
|
|
2287
|
+
});
|
|
2288
|
+
|
|
2219
2289
|
// src/server/ai/anthropic.ts
|
|
2220
2290
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2221
2291
|
var AnthropicProvider;
|
|
@@ -2224,6 +2294,7 @@ var init_anthropic = __esm({
|
|
|
2224
2294
|
"use strict";
|
|
2225
2295
|
init_provider();
|
|
2226
2296
|
init_batch();
|
|
2297
|
+
init_pricing();
|
|
2227
2298
|
AnthropicProvider = class {
|
|
2228
2299
|
constructor(config, client) {
|
|
2229
2300
|
this.config = config;
|
|
@@ -2238,9 +2309,25 @@ var init_anthropic = __esm({
|
|
|
2238
2309
|
}
|
|
2239
2310
|
config;
|
|
2240
2311
|
client;
|
|
2312
|
+
usage = { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
|
|
2241
2313
|
supportsVision() {
|
|
2242
2314
|
return true;
|
|
2243
2315
|
}
|
|
2316
|
+
recordUsage(usage) {
|
|
2317
|
+
if (!usage) return;
|
|
2318
|
+
addUsage(this.usage, {
|
|
2319
|
+
inputTokens: usage.input_tokens ?? 0,
|
|
2320
|
+
outputTokens: usage.output_tokens ?? 0,
|
|
2321
|
+
cacheCreationInputTokens: usage.cache_creation_input_tokens ?? 0,
|
|
2322
|
+
cacheReadInputTokens: usage.cache_read_input_tokens ?? 0
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
takeUsage() {
|
|
2326
|
+
const taken = this.usage;
|
|
2327
|
+
this.usage = { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
|
|
2328
|
+
const any = taken.inputTokens || taken.outputTokens || taken.cacheCreationInputTokens || taken.cacheReadInputTokens;
|
|
2329
|
+
return any ? taken : void 0;
|
|
2330
|
+
}
|
|
2244
2331
|
translate(reqs, onBatchComplete, signal, onMalformedReply) {
|
|
2245
2332
|
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal, onMalformedReply);
|
|
2246
2333
|
}
|
|
@@ -2262,10 +2349,13 @@ var init_anthropic = __esm({
|
|
|
2262
2349
|
content.push({ type: "text", text: buildBatchPrompt(batch) });
|
|
2263
2350
|
return content;
|
|
2264
2351
|
}
|
|
2265
|
-
|
|
2266
|
-
|
|
2352
|
+
completionContent(req) {
|
|
2353
|
+
return req.content.map(
|
|
2267
2354
|
(b) => b.type === "image" ? { type: "image", source: { type: "base64", media_type: b.mediaType, data: b.base64 } } : { type: "text", text: b.text ?? "" }
|
|
2268
2355
|
);
|
|
2356
|
+
}
|
|
2357
|
+
async complete(req) {
|
|
2358
|
+
const content = this.completionContent(req);
|
|
2269
2359
|
const res = await this.client.messages.create({
|
|
2270
2360
|
model: this.config.model,
|
|
2271
2361
|
max_tokens: req.maxTokens ?? 8192,
|
|
@@ -2273,6 +2363,7 @@ var init_anthropic = __esm({
|
|
|
2273
2363
|
output_config: { format: { type: "json_schema", schema: req.schema } },
|
|
2274
2364
|
messages: [{ role: "user", content }]
|
|
2275
2365
|
});
|
|
2366
|
+
this.recordUsage(res.usage);
|
|
2276
2367
|
const text = res.content.find((b) => b.type === "text")?.text ?? "{}";
|
|
2277
2368
|
try {
|
|
2278
2369
|
return JSON.parse(text);
|
|
@@ -2314,6 +2405,7 @@ var init_anthropic = __esm({
|
|
|
2314
2405
|
out.set(entry.custom_id, { type: "failed", error: entry.result.error?.message ?? entry.result.type });
|
|
2315
2406
|
continue;
|
|
2316
2407
|
}
|
|
2408
|
+
this.recordUsage(entry.result.message?.usage);
|
|
2317
2409
|
const text = entry.result.message?.content.find((b) => b.type === "text")?.text ?? "";
|
|
2318
2410
|
try {
|
|
2319
2411
|
out.set(entry.custom_id, { type: "items", items: parseReplyItems(text) });
|
|
@@ -2327,6 +2419,40 @@ var init_anthropic = __esm({
|
|
|
2327
2419
|
async cancelTranslationBatch(batchId) {
|
|
2328
2420
|
await this.batchesClient().cancel(batchId);
|
|
2329
2421
|
}
|
|
2422
|
+
// Mirrors complete() exactly — same prompts and schema — so batch and sync
|
|
2423
|
+
// completion replies are interchangeable downstream.
|
|
2424
|
+
async submitCompletionBatch(jobs) {
|
|
2425
|
+
const requests = jobs.map((job) => ({
|
|
2426
|
+
custom_id: job.customId,
|
|
2427
|
+
params: {
|
|
2428
|
+
model: this.config.model,
|
|
2429
|
+
max_tokens: job.request.maxTokens ?? 8192,
|
|
2430
|
+
// Batch entries don't share a live cache window, so cache_control is omitted here.
|
|
2431
|
+
system: [{ type: "text", text: job.request.system }],
|
|
2432
|
+
output_config: { format: { type: "json_schema", schema: job.request.schema } },
|
|
2433
|
+
messages: [{ role: "user", content: this.completionContent(job.request) }]
|
|
2434
|
+
}
|
|
2435
|
+
}));
|
|
2436
|
+
const res = await this.batchesClient().create({ requests });
|
|
2437
|
+
return res.id;
|
|
2438
|
+
}
|
|
2439
|
+
async completionBatchResults(batchId) {
|
|
2440
|
+
const out = /* @__PURE__ */ new Map();
|
|
2441
|
+
for await (const entry of await this.batchesClient().results(batchId)) {
|
|
2442
|
+
if (entry.result.type !== "succeeded") {
|
|
2443
|
+
out.set(entry.custom_id, { type: "failed", error: entry.result.error?.message ?? entry.result.type });
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
this.recordUsage(entry.result.message?.usage);
|
|
2447
|
+
const text = entry.result.message?.content.find((b) => b.type === "text")?.text ?? "";
|
|
2448
|
+
try {
|
|
2449
|
+
out.set(entry.custom_id, { type: "json", value: JSON.parse(text) });
|
|
2450
|
+
} catch {
|
|
2451
|
+
out.set(entry.custom_id, { type: "malformed", raw: text });
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
return out;
|
|
2455
|
+
}
|
|
2330
2456
|
async callBatch(batch, signal) {
|
|
2331
2457
|
const content = this.buildUserContent(batch);
|
|
2332
2458
|
const res = await this.client.messages.create({
|
|
@@ -2336,6 +2462,7 @@ var init_anthropic = __esm({
|
|
|
2336
2462
|
output_config: { format: { type: "json_schema", schema: BATCH_SCHEMA } },
|
|
2337
2463
|
messages: [{ role: "user", content }]
|
|
2338
2464
|
}, { signal });
|
|
2465
|
+
this.recordUsage(res.usage);
|
|
2339
2466
|
const text = res.content.find((b) => b.type === "text")?.text ?? "";
|
|
2340
2467
|
return parseReplyItems(text);
|
|
2341
2468
|
}
|
|
@@ -3133,6 +3260,30 @@ var init_pending_batch = __esm({
|
|
|
3133
3260
|
}
|
|
3134
3261
|
});
|
|
3135
3262
|
|
|
3263
|
+
// src/server/log.ts
|
|
3264
|
+
import { appendFileSync, readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
3265
|
+
import { resolve as resolve5 } from "path";
|
|
3266
|
+
function logPath(projectRoot) {
|
|
3267
|
+
return resolve5(projectRoot, ".glotfile", "log.jsonl");
|
|
3268
|
+
}
|
|
3269
|
+
function appendLog(projectRoot, entry) {
|
|
3270
|
+
ensureGlotfileDir(projectRoot);
|
|
3271
|
+
appendFileSync(logPath(projectRoot), JSON.stringify(entry) + "\n", "utf8");
|
|
3272
|
+
}
|
|
3273
|
+
function readLog(projectRoot, limit = 100) {
|
|
3274
|
+
const path = logPath(projectRoot);
|
|
3275
|
+
if (!existsSync7(path)) return [];
|
|
3276
|
+
const lines = readFileSync7(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
3277
|
+
const entries = lines.map((l) => JSON.parse(l));
|
|
3278
|
+
return entries.reverse().slice(0, limit);
|
|
3279
|
+
}
|
|
3280
|
+
var init_log = __esm({
|
|
3281
|
+
"src/server/log.ts"() {
|
|
3282
|
+
"use strict";
|
|
3283
|
+
init_glotfile_dir();
|
|
3284
|
+
}
|
|
3285
|
+
});
|
|
3286
|
+
|
|
3136
3287
|
// src/server/ai/batch-run.ts
|
|
3137
3288
|
function buildBatchJobs(reqs, batchSize) {
|
|
3138
3289
|
const byLocale = /* @__PURE__ */ new Map();
|
|
@@ -3180,7 +3331,9 @@ async function submitBatchTranslation(state, provider, reqs, batchSize, model, p
|
|
|
3180
3331
|
return pending;
|
|
3181
3332
|
}
|
|
3182
3333
|
async function applyBatchResults(load, persist, provider, pending, projectRoot, ai) {
|
|
3334
|
+
provider.takeUsage?.();
|
|
3183
3335
|
const outcomes = await provider.translationBatchResults(pending.batchId);
|
|
3336
|
+
const batchUsage = provider.takeUsage?.();
|
|
3184
3337
|
const fresh = load();
|
|
3185
3338
|
const isStale = (r) => {
|
|
3186
3339
|
const entry = fresh.keys[r.key];
|
|
@@ -3189,13 +3342,19 @@ async function applyBatchResults(load, persist, provider, pending, projectRoot,
|
|
|
3189
3342
|
const applied = [];
|
|
3190
3343
|
const results = [];
|
|
3191
3344
|
const retryReqs = [];
|
|
3192
|
-
|
|
3345
|
+
const stale = [];
|
|
3346
|
+
const jobFailures = [];
|
|
3193
3347
|
for (const job of pending.jobs) {
|
|
3194
3348
|
const outcome = outcomes.get(job.customId);
|
|
3195
3349
|
const itemsById = outcome?.type === "items" ? new Map(outcome.items.map((i) => [i.id, i])) : null;
|
|
3350
|
+
if (!itemsById) {
|
|
3351
|
+
if (!outcome) jobFailures.push({ customId: job.customId, locale: job.locale, type: "missing" });
|
|
3352
|
+
else if (outcome.type === "malformed") jobFailures.push({ customId: job.customId, locale: job.locale, type: "malformed", raw: outcome.raw });
|
|
3353
|
+
else if (outcome.type === "failed") jobFailures.push({ customId: job.customId, locale: job.locale, type: "failed", error: outcome.error });
|
|
3354
|
+
}
|
|
3196
3355
|
for (const stored of job.requests) {
|
|
3197
3356
|
if (isStale(stored)) {
|
|
3198
|
-
|
|
3357
|
+
stale.push({ key: stored.key, locale: stored.targetLocale });
|
|
3199
3358
|
continue;
|
|
3200
3359
|
}
|
|
3201
3360
|
const { sourceHash: _hash, ...req } = stored;
|
|
@@ -3214,7 +3373,20 @@ async function applyBatchResults(load, persist, provider, pending, projectRoot,
|
|
|
3214
3373
|
const retryResults = await runLocaleParallel(
|
|
3215
3374
|
retryReqs,
|
|
3216
3375
|
provider,
|
|
3217
|
-
{
|
|
3376
|
+
{
|
|
3377
|
+
// Record the raw reply so an unparseable retry response is diagnosable
|
|
3378
|
+
// from the activity log instead of vanishing into per-item errors.
|
|
3379
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
3380
|
+
appendLog(projectRoot, {
|
|
3381
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3382
|
+
kind: "translate",
|
|
3383
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
3384
|
+
model: pending.model,
|
|
3385
|
+
locale,
|
|
3386
|
+
raw
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
3389
|
+
},
|
|
3218
3390
|
ai.concurrency,
|
|
3219
3391
|
void 0,
|
|
3220
3392
|
ai.batchSize
|
|
@@ -3222,10 +3394,34 @@ async function applyBatchResults(load, persist, provider, pending, projectRoot,
|
|
|
3222
3394
|
applied.push(...retryReqs);
|
|
3223
3395
|
results.push(...retryResults);
|
|
3224
3396
|
}
|
|
3397
|
+
const retryUsage = provider.takeUsage?.();
|
|
3398
|
+
const pricing = resolvePricing({ ...ai, model: pending.model });
|
|
3399
|
+
let estimatedCostUsd;
|
|
3400
|
+
if (pricing && (batchUsage || retryUsage)) {
|
|
3401
|
+
estimatedCostUsd = (batchUsage ? estimateUsageCostUsd(batchUsage, pricing, BATCH_PRICE_MULTIPLIER) : 0) + (retryUsage ? estimateUsageCostUsd(retryUsage, pricing) : 0);
|
|
3402
|
+
}
|
|
3403
|
+
let usage;
|
|
3404
|
+
if (batchUsage || retryUsage) {
|
|
3405
|
+
usage = batchUsage ?? { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
|
|
3406
|
+
if (retryUsage) addUsage(usage, retryUsage);
|
|
3407
|
+
}
|
|
3225
3408
|
const { written, errors } = applyResults(fresh, applied, results);
|
|
3226
3409
|
persist(fresh);
|
|
3227
3410
|
clearPendingBatch(projectRoot);
|
|
3228
|
-
|
|
3411
|
+
const costSuffix = estimatedCostUsd !== void 0 ? ` (~$${estimatedCostUsd.toFixed(2)})` : "";
|
|
3412
|
+
appendLog(projectRoot, {
|
|
3413
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3414
|
+
kind: "translate",
|
|
3415
|
+
summary: `Applied batch ${pending.batchId}: wrote ${written}, ${errors.length} error(s), ${retryReqs.length} retried, ${stale.length} stale${costSuffix}`,
|
|
3416
|
+
model: pending.model,
|
|
3417
|
+
items: applied.map((r) => ({ id: r.id, key: r.key, source: r.source, targetLocale: r.targetLocale })),
|
|
3418
|
+
results,
|
|
3419
|
+
jobFailures: jobFailures.length ? jobFailures : void 0,
|
|
3420
|
+
stale: stale.length ? stale : void 0,
|
|
3421
|
+
usage,
|
|
3422
|
+
estimatedCostUsd
|
|
3423
|
+
});
|
|
3424
|
+
return { written, errors, staleSkipped: stale.length, retried: retryReqs.length, screenshotsSkipped };
|
|
3229
3425
|
}
|
|
3230
3426
|
var init_batch_run = __esm({
|
|
3231
3427
|
"src/server/ai/batch-run.ts"() {
|
|
@@ -3234,97 +3430,352 @@ var init_batch_run = __esm({
|
|
|
3234
3430
|
init_batch();
|
|
3235
3431
|
init_run();
|
|
3236
3432
|
init_pending_batch();
|
|
3433
|
+
init_log();
|
|
3434
|
+
init_pricing();
|
|
3237
3435
|
}
|
|
3238
3436
|
});
|
|
3239
3437
|
|
|
3240
|
-
// src/server/ai/
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
if (anth !== -1) id = id.slice(anth + "anthropic.".length);
|
|
3247
|
-
return id;
|
|
3248
|
-
}
|
|
3249
|
-
function resolvePricing(ai) {
|
|
3250
|
-
if (ai.inputPricePerMTok !== void 0 && ai.outputPricePerMTok !== void 0) {
|
|
3251
|
-
return { source: "profile", inputPerMTok: ai.inputPricePerMTok, outputPerMTok: ai.outputPricePerMTok };
|
|
3252
|
-
}
|
|
3253
|
-
if (FREE_PROVIDERS.has(ai.provider)) return { source: "builtin", inputPerMTok: 0, outputPerMTok: 0 };
|
|
3254
|
-
const id = bareModelId(ai.model);
|
|
3255
|
-
let best;
|
|
3256
|
-
for (const row of PRICE_TABLE) {
|
|
3257
|
-
if (id.startsWith(row[0]) && (!best || row[0].length > best[0].length)) best = row;
|
|
3258
|
-
}
|
|
3259
|
-
return best ? { source: "builtin", inputPerMTok: best[1], outputPerMTok: best[2] } : null;
|
|
3260
|
-
}
|
|
3261
|
-
var PRICE_TABLE, FREE_PROVIDERS;
|
|
3262
|
-
var init_pricing = __esm({
|
|
3263
|
-
"src/server/ai/pricing.ts"() {
|
|
3264
|
-
"use strict";
|
|
3265
|
-
PRICE_TABLE = [
|
|
3266
|
-
["claude-fable-5", 10, 50],
|
|
3267
|
-
["claude-mythos-5", 10, 50],
|
|
3268
|
-
// Deprecated Opus 4.1 / 4.0 cost 3x the 4.5+ generation — they must outrank
|
|
3269
|
-
// the shorter "claude-opus-4" prefix (4-2025 covers the dated Opus 4 full IDs).
|
|
3270
|
-
["claude-opus-4-1", 15, 75],
|
|
3271
|
-
["claude-opus-4-0", 15, 75],
|
|
3272
|
-
["claude-opus-4-2025", 15, 75],
|
|
3273
|
-
["claude-opus-4", 5, 25],
|
|
3274
|
-
["claude-sonnet-4", 3, 15],
|
|
3275
|
-
["claude-haiku-4", 1, 5],
|
|
3276
|
-
["claude-3-5-haiku", 0.8, 4],
|
|
3277
|
-
["gpt-5.5-pro", 30, 180],
|
|
3278
|
-
["gpt-5.5", 5, 30],
|
|
3279
|
-
["gpt-5.4-pro", 30, 180],
|
|
3280
|
-
["gpt-5.4-mini", 0.75, 4.5],
|
|
3281
|
-
["gpt-5.4-nano", 0.2, 1.25],
|
|
3282
|
-
["gpt-5.4", 2.5, 15],
|
|
3283
|
-
["gpt-5.3-codex", 1.75, 14]
|
|
3284
|
-
];
|
|
3285
|
-
FREE_PROVIDERS = /* @__PURE__ */ new Set(["ollama", "claude-code"]);
|
|
3286
|
-
}
|
|
3287
|
-
});
|
|
3288
|
-
|
|
3289
|
-
// src/server/ai/estimate.ts
|
|
3290
|
-
function estimateTokens(text) {
|
|
3291
|
-
const cjk = text.match(CJK_RE)?.length ?? 0;
|
|
3292
|
-
return Math.ceil((text.length - cjk) / 4 + cjk / 2);
|
|
3438
|
+
// src/server/ai/context.ts
|
|
3439
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
3440
|
+
import { resolve as resolve6 } from "path";
|
|
3441
|
+
function globToRegExp2(glob) {
|
|
3442
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3443
|
+
return new RegExp(`^${escaped}$`);
|
|
3293
3444
|
}
|
|
3294
|
-
function
|
|
3295
|
-
const
|
|
3296
|
-
|
|
3297
|
-
|
|
3445
|
+
function extractSnippets(refs, projectRoot, fileCache) {
|
|
3446
|
+
const filtered = refs.filter((r) => !EXCLUDED_DIRS.some((d) => r.file.startsWith(d)));
|
|
3447
|
+
const sorted = [...filtered].sort((a, b) => a.file.length - b.file.length);
|
|
3448
|
+
const selected = sorted.slice(0, MAX_SNIPPETS);
|
|
3449
|
+
const extraRefs = filtered.length > MAX_SNIPPETS ? filtered.length - MAX_SNIPPETS : 0;
|
|
3450
|
+
const snippets = [];
|
|
3451
|
+
for (const ref of selected) {
|
|
3452
|
+
const absPath = resolve6(projectRoot, ref.file);
|
|
3453
|
+
if (!fileCache.has(ref.file)) {
|
|
3454
|
+
if (!existsSync8(absPath)) continue;
|
|
3455
|
+
const content = readFileSync8(absPath, "utf8");
|
|
3456
|
+
fileCache.set(ref.file, content.split("\n"));
|
|
3457
|
+
}
|
|
3458
|
+
const lines = fileCache.get(ref.file);
|
|
3459
|
+
const start = Math.max(0, ref.line - 1 - SNIPPET_WINDOW);
|
|
3460
|
+
const end = Math.min(lines.length, ref.line + SNIPPET_WINDOW);
|
|
3461
|
+
snippets.push({
|
|
3462
|
+
file: ref.file,
|
|
3463
|
+
startLine: start + 1,
|
|
3464
|
+
lines: lines.slice(start, end).join("\n"),
|
|
3465
|
+
scanner: ref.scanner,
|
|
3466
|
+
...snippets.length === 0 && extraRefs > 0 ? { extraRefs } : {}
|
|
3467
|
+
});
|
|
3298
3468
|
}
|
|
3299
|
-
return
|
|
3469
|
+
return snippets;
|
|
3300
3470
|
}
|
|
3301
|
-
function
|
|
3302
|
-
const
|
|
3303
|
-
const
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
byLocale.set(r.targetLocale, group);
|
|
3471
|
+
function buildUsageIndex(cache2) {
|
|
3472
|
+
const index = /* @__PURE__ */ new Map();
|
|
3473
|
+
for (const [file, entry] of Object.entries(cache2.files)) {
|
|
3474
|
+
for (const ref of entry.refs) {
|
|
3475
|
+
const existing = index.get(ref.key) ?? [];
|
|
3476
|
+
existing.push({ key: ref.key, file, line: ref.line, col: ref.col, scanner: ref.scanner });
|
|
3477
|
+
index.set(ref.key, existing);
|
|
3309
3478
|
}
|
|
3310
|
-
group.push(r);
|
|
3311
3479
|
}
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3480
|
+
return index;
|
|
3481
|
+
}
|
|
3482
|
+
function selectContextTargets(state, opts, cache2, lastRunAt) {
|
|
3483
|
+
const cutoff = opts.all ? void 0 : opts.since ?? lastRunAt;
|
|
3484
|
+
const keyRe = opts.keyGlob ? globToRegExp2(opts.keyGlob) : null;
|
|
3485
|
+
const keySet = opts.keys ? new Set(opts.keys) : null;
|
|
3486
|
+
const usageIndex = buildUsageIndex(cache2);
|
|
3487
|
+
let candidates = [];
|
|
3488
|
+
for (const key of Object.keys(state.keys).sort()) {
|
|
3489
|
+
const entry = state.keys[key];
|
|
3490
|
+
if (entry.context && !opts.force) continue;
|
|
3491
|
+
if (keySet && !keySet.has(key)) continue;
|
|
3492
|
+
if (keyRe && !keyRe.test(key)) continue;
|
|
3493
|
+
if (cutoff) {
|
|
3494
|
+
if (!entry.createdAt) continue;
|
|
3495
|
+
if (entry.createdAt < cutoff) continue;
|
|
3321
3496
|
}
|
|
3322
|
-
|
|
3497
|
+
const source = entry.values[state.config.sourceLocale]?.value ?? "";
|
|
3498
|
+
candidates.push({ id: String(candidates.length), key, source, usageSnippets: [] });
|
|
3323
3499
|
}
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3500
|
+
candidates.sort((a, b) => {
|
|
3501
|
+
const ta = state.keys[a.key].createdAt ?? "";
|
|
3502
|
+
const tb = state.keys[b.key].createdAt ?? "";
|
|
3503
|
+
return tb.localeCompare(ta);
|
|
3504
|
+
});
|
|
3505
|
+
if (opts.limit !== void 0) candidates = candidates.slice(0, opts.limit);
|
|
3506
|
+
candidates.forEach((c, i) => {
|
|
3507
|
+
c.id = String(i);
|
|
3508
|
+
});
|
|
3509
|
+
return candidates;
|
|
3510
|
+
}
|
|
3511
|
+
function buildContextSystemPrompt() {
|
|
3512
|
+
return [
|
|
3513
|
+
"You are a localization context writer for a UI string catalog.",
|
|
3514
|
+
"For each translation key you are given: its dot-path name, its source string, and one or more code snippets showing where the string is referenced in the codebase.",
|
|
3515
|
+
"Your task: write a concise 1\u20132 sentence context note that describes WHERE in the UI this string appears and WHAT the user is doing at that point.",
|
|
3516
|
+
"The context is read by human translators AND by an AI translation engine. It must answer: what screen is this on, what element is this (button, label, error, etc.), and what action does it relate to?",
|
|
3517
|
+
"Rules:",
|
|
3518
|
+
"- Use the code snippets as your primary signal. Look at the component name, surrounding labels, event handlers, and variable names.",
|
|
3519
|
+
"- Do NOT restate the source string itself.",
|
|
3520
|
+
"- Do NOT say 'This string is...' \u2014 write the context as a direct description.",
|
|
3521
|
+
"- Keep it under 500 characters.",
|
|
3522
|
+
"- If no code snippets are available, infer from the key path and source value."
|
|
3523
|
+
].join("\n");
|
|
3524
|
+
}
|
|
3525
|
+
function buildContextBatchPrompt(reqs) {
|
|
3526
|
+
const items = reqs.map((r) => {
|
|
3527
|
+
const snippetText = r.usageSnippets.length > 0 ? r.usageSnippets.map((s) => {
|
|
3528
|
+
const extra = s.extraRefs ? ` (and ${s.extraRefs} more call site${s.extraRefs > 1 ? "s" : ""} not shown)` : "";
|
|
3529
|
+
return `File: ${s.file} (lines ${s.startLine}+, scanner: ${s.scanner})${extra}
|
|
3530
|
+
\`\`\`
|
|
3531
|
+
${s.lines}
|
|
3532
|
+
\`\`\``;
|
|
3533
|
+
}).join("\n\n") : "(no code references found \u2014 infer from key path and source value)";
|
|
3534
|
+
return { id: r.id, key: r.key, source: r.source, codeSnippets: snippetText };
|
|
3535
|
+
});
|
|
3536
|
+
return 'Write a context note for each key. Return JSON {"items":[{"id","context"}]}.\n' + JSON.stringify(items, null, 2);
|
|
3537
|
+
}
|
|
3538
|
+
function applyContext(state, reqs, results, clock = systemClock, force = false) {
|
|
3539
|
+
const byId = new Map(reqs.map((r) => [r.id, r]));
|
|
3540
|
+
let written = 0;
|
|
3541
|
+
const errors = [];
|
|
3542
|
+
for (const res of results) {
|
|
3543
|
+
const req = byId.get(res.id);
|
|
3544
|
+
if (!req) continue;
|
|
3545
|
+
if (res.error) {
|
|
3546
|
+
errors.push({ key: req.key, error: res.error });
|
|
3547
|
+
continue;
|
|
3548
|
+
}
|
|
3549
|
+
const context = res.context?.trim() ?? "";
|
|
3550
|
+
if (!context) {
|
|
3551
|
+
errors.push({ key: req.key, error: "AI returned empty context" });
|
|
3552
|
+
continue;
|
|
3553
|
+
}
|
|
3554
|
+
if (context.length > MAX_CONTEXT_LENGTH) {
|
|
3555
|
+
errors.push({ key: req.key, error: `Context too long (${context.length} chars, max ${MAX_CONTEXT_LENGTH})` });
|
|
3556
|
+
continue;
|
|
3557
|
+
}
|
|
3558
|
+
const entry = state.keys[req.key];
|
|
3559
|
+
if (!entry || entry.context && !force) continue;
|
|
3560
|
+
entry.context = context;
|
|
3561
|
+
entry.contextSource = "ai";
|
|
3562
|
+
entry.contextAt = clock();
|
|
3563
|
+
written++;
|
|
3564
|
+
}
|
|
3565
|
+
return { written, errors };
|
|
3566
|
+
}
|
|
3567
|
+
var MAX_CONTEXT_LENGTH, SNIPPET_WINDOW, MAX_SNIPPETS, EXCLUDED_DIRS, CONTEXT_BATCH_SCHEMA;
|
|
3568
|
+
var init_context = __esm({
|
|
3569
|
+
"src/server/ai/context.ts"() {
|
|
3570
|
+
"use strict";
|
|
3571
|
+
init_state();
|
|
3572
|
+
MAX_CONTEXT_LENGTH = 500;
|
|
3573
|
+
SNIPPET_WINDOW = 15;
|
|
3574
|
+
MAX_SNIPPETS = 3;
|
|
3575
|
+
EXCLUDED_DIRS = ["node_modules/", "vendor/", "dist/", ".git/", ".glotfile/"];
|
|
3576
|
+
CONTEXT_BATCH_SCHEMA = {
|
|
3577
|
+
type: "object",
|
|
3578
|
+
properties: {
|
|
3579
|
+
items: {
|
|
3580
|
+
type: "array",
|
|
3581
|
+
items: {
|
|
3582
|
+
type: "object",
|
|
3583
|
+
properties: {
|
|
3584
|
+
id: { type: "string" },
|
|
3585
|
+
context: { type: "string" },
|
|
3586
|
+
error: { type: "string" }
|
|
3587
|
+
},
|
|
3588
|
+
required: ["id"],
|
|
3589
|
+
additionalProperties: false
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
},
|
|
3593
|
+
required: ["items"],
|
|
3594
|
+
additionalProperties: false
|
|
3595
|
+
};
|
|
3596
|
+
}
|
|
3597
|
+
});
|
|
3598
|
+
|
|
3599
|
+
// src/server/ai/pending-context-batch.ts
|
|
3600
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync4, rmSync as rmSync5 } from "fs";
|
|
3601
|
+
import { join as join4 } from "path";
|
|
3602
|
+
function pendingContextBatchPath(projectRoot) {
|
|
3603
|
+
return join4(projectRoot, ".glotfile", "context-batch.json");
|
|
3604
|
+
}
|
|
3605
|
+
function loadPendingContextBatch(projectRoot) {
|
|
3606
|
+
const path = pendingContextBatchPath(projectRoot);
|
|
3607
|
+
if (!existsSync9(path)) return void 0;
|
|
3608
|
+
try {
|
|
3609
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
3610
|
+
if (parsed?.version !== 1) return void 0;
|
|
3611
|
+
return parsed;
|
|
3612
|
+
} catch {
|
|
3613
|
+
return void 0;
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
function savePendingContextBatch(projectRoot, pending) {
|
|
3617
|
+
const dir = join4(projectRoot, ".glotfile");
|
|
3618
|
+
mkdirSync5(dir, { recursive: true });
|
|
3619
|
+
const gitignore = join4(dir, ".gitignore");
|
|
3620
|
+
if (!existsSync9(gitignore)) writeFileSync4(gitignore, "*\n");
|
|
3621
|
+
writeFileSync4(pendingContextBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
|
|
3622
|
+
}
|
|
3623
|
+
function clearPendingContextBatch(projectRoot) {
|
|
3624
|
+
rmSync5(pendingContextBatchPath(projectRoot), { force: true });
|
|
3625
|
+
}
|
|
3626
|
+
var init_pending_context_batch = __esm({
|
|
3627
|
+
"src/server/ai/pending-context-batch.ts"() {
|
|
3628
|
+
"use strict";
|
|
3629
|
+
}
|
|
3630
|
+
});
|
|
3631
|
+
|
|
3632
|
+
// src/server/ai/context-batch-run.ts
|
|
3633
|
+
function completionRequestFor(chunk2) {
|
|
3634
|
+
return {
|
|
3635
|
+
system: buildContextSystemPrompt(),
|
|
3636
|
+
content: [{ type: "text", text: buildContextBatchPrompt(chunk2) }],
|
|
3637
|
+
schema: CONTEXT_BATCH_SCHEMA
|
|
3638
|
+
};
|
|
3639
|
+
}
|
|
3640
|
+
async function submitContextBatch(provider, targets, batchSize, model, projectRoot, force) {
|
|
3641
|
+
if (loadPendingContextBatch(projectRoot)) {
|
|
3642
|
+
throw new Error("A context batch is already pending. Apply or cancel it first.");
|
|
3643
|
+
}
|
|
3644
|
+
const chunks = [];
|
|
3645
|
+
const size = Math.max(1, batchSize);
|
|
3646
|
+
for (let i = 0; i < targets.length; i += size) chunks.push(targets.slice(i, i + size));
|
|
3647
|
+
const jobs = chunks.map((chunk2, i) => ({ customId: `ctx_${i}`, chunk: chunk2 }));
|
|
3648
|
+
const batchId = await provider.submitCompletionBatch(
|
|
3649
|
+
jobs.map((j) => ({ customId: j.customId, request: completionRequestFor(j.chunk) }))
|
|
3650
|
+
);
|
|
3651
|
+
const pending = {
|
|
3652
|
+
version: 1,
|
|
3653
|
+
// Only Anthropic implements completion batches today.
|
|
3654
|
+
provider: "anthropic",
|
|
3655
|
+
model,
|
|
3656
|
+
batchId,
|
|
3657
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3658
|
+
total: targets.length,
|
|
3659
|
+
force,
|
|
3660
|
+
jobs: jobs.map((j) => ({
|
|
3661
|
+
customId: j.customId,
|
|
3662
|
+
requests: j.chunk.map(({ image: _image, ...rest }) => rest)
|
|
3663
|
+
}))
|
|
3664
|
+
};
|
|
3665
|
+
savePendingContextBatch(projectRoot, pending);
|
|
3666
|
+
return pending;
|
|
3667
|
+
}
|
|
3668
|
+
async function applyContextBatchResults(load, persist, provider, pending, projectRoot, ai) {
|
|
3669
|
+
provider.takeUsage?.();
|
|
3670
|
+
const outcomes = await provider.completionBatchResults(pending.batchId);
|
|
3671
|
+
const batchUsage = provider.takeUsage?.();
|
|
3672
|
+
const applied = [];
|
|
3673
|
+
const items = [];
|
|
3674
|
+
const errors = [];
|
|
3675
|
+
const jobFailures = [];
|
|
3676
|
+
const retryChunks = [];
|
|
3677
|
+
for (const job of pending.jobs) {
|
|
3678
|
+
const outcome = outcomes.get(job.customId);
|
|
3679
|
+
if (outcome?.type === "json") {
|
|
3680
|
+
const batch = outcome.value;
|
|
3681
|
+
applied.push(...job.requests);
|
|
3682
|
+
items.push(...batch.items ?? []);
|
|
3683
|
+
continue;
|
|
3684
|
+
}
|
|
3685
|
+
if (!outcome) jobFailures.push({ customId: job.customId, locale: "", type: "missing" });
|
|
3686
|
+
else if (outcome.type === "malformed") jobFailures.push({ customId: job.customId, locale: "", type: "malformed", raw: outcome.raw });
|
|
3687
|
+
else jobFailures.push({ customId: job.customId, locale: "", type: "failed", error: outcome.error });
|
|
3688
|
+
retryChunks.push(job.requests);
|
|
3689
|
+
}
|
|
3690
|
+
for (const chunk2 of retryChunks) {
|
|
3691
|
+
try {
|
|
3692
|
+
const raw = await provider.complete(completionRequestFor(chunk2));
|
|
3693
|
+
const batch = raw;
|
|
3694
|
+
applied.push(...chunk2);
|
|
3695
|
+
items.push(...batch.items ?? []);
|
|
3696
|
+
} catch (e) {
|
|
3697
|
+
errors.push(...chunk2.map((t) => ({ key: t.key, error: e.message })));
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
const retryUsage = provider.takeUsage?.();
|
|
3701
|
+
const pricing = resolvePricing({ ...ai, model: pending.model });
|
|
3702
|
+
let estimatedCostUsd;
|
|
3703
|
+
if (pricing && (batchUsage || retryUsage)) {
|
|
3704
|
+
estimatedCostUsd = (batchUsage ? estimateUsageCostUsd(batchUsage, pricing, BATCH_PRICE_MULTIPLIER) : 0) + (retryUsage ? estimateUsageCostUsd(retryUsage, pricing) : 0);
|
|
3705
|
+
}
|
|
3706
|
+
let usage;
|
|
3707
|
+
if (batchUsage || retryUsage) {
|
|
3708
|
+
usage = batchUsage ?? { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
|
|
3709
|
+
if (retryUsage) addUsage(usage, retryUsage);
|
|
3710
|
+
}
|
|
3711
|
+
const fresh = load();
|
|
3712
|
+
const { written, errors: applyErrors } = applyContext(fresh, applied, items, void 0, pending.force);
|
|
3713
|
+
errors.push(...applyErrors);
|
|
3714
|
+
persist(fresh);
|
|
3715
|
+
clearPendingContextBatch(projectRoot);
|
|
3716
|
+
const costSuffix = estimatedCostUsd !== void 0 ? ` (~$${estimatedCostUsd.toFixed(2)})` : "";
|
|
3717
|
+
appendLog(projectRoot, {
|
|
3718
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3719
|
+
kind: "context",
|
|
3720
|
+
summary: `Applied context batch ${pending.batchId}: wrote ${written}, ${errors.length} error(s), ${retryChunks.length} job(s) retried${costSuffix}`,
|
|
3721
|
+
model: pending.model,
|
|
3722
|
+
items: applied.map((r) => ({ id: r.id, key: r.key, source: r.source })),
|
|
3723
|
+
results: items.map((r) => ({ id: r.id, value: r.context, error: r.error })),
|
|
3724
|
+
jobFailures: jobFailures.length ? jobFailures : void 0,
|
|
3725
|
+
usage,
|
|
3726
|
+
estimatedCostUsd
|
|
3727
|
+
});
|
|
3728
|
+
return { written, errors, retried: retryChunks.length };
|
|
3729
|
+
}
|
|
3730
|
+
var init_context_batch_run = __esm({
|
|
3731
|
+
"src/server/ai/context-batch-run.ts"() {
|
|
3732
|
+
"use strict";
|
|
3733
|
+
init_context();
|
|
3734
|
+
init_pending_context_batch();
|
|
3735
|
+
init_log();
|
|
3736
|
+
init_pricing();
|
|
3737
|
+
}
|
|
3738
|
+
});
|
|
3739
|
+
|
|
3740
|
+
// src/server/ai/estimate.ts
|
|
3741
|
+
function estimateTokens(text) {
|
|
3742
|
+
const cjk = text.match(CJK_RE)?.length ?? 0;
|
|
3743
|
+
return Math.ceil((text.length - cjk) / 4 + cjk / 2);
|
|
3744
|
+
}
|
|
3745
|
+
function estimateOutputTokens(req) {
|
|
3746
|
+
const translated = Math.ceil(estimateTokens(req.source) * EXPANSION);
|
|
3747
|
+
if (req.plural) {
|
|
3748
|
+
return ITEM_REPLY_OVERHEAD + req.plural.categories.length * (translated + FORM_REPLY_OVERHEAD);
|
|
3749
|
+
}
|
|
3750
|
+
return ITEM_REPLY_OVERHEAD + translated;
|
|
3751
|
+
}
|
|
3752
|
+
function estimateTranslation(state, ai, opts) {
|
|
3753
|
+
const reqs = selectRequests(state, opts);
|
|
3754
|
+
const byLocale = /* @__PURE__ */ new Map();
|
|
3755
|
+
for (const r of reqs) {
|
|
3756
|
+
let group = byLocale.get(r.targetLocale);
|
|
3757
|
+
if (!group) {
|
|
3758
|
+
group = [];
|
|
3759
|
+
byLocale.set(r.targetLocale, group);
|
|
3760
|
+
}
|
|
3761
|
+
group.push(r);
|
|
3762
|
+
}
|
|
3763
|
+
const perLocale = [];
|
|
3764
|
+
for (const [locale, group] of byLocale) {
|
|
3765
|
+
let inputTokens2 = 0;
|
|
3766
|
+
let outputTokens2 = 0;
|
|
3767
|
+
const batches = chunk(group, Math.max(1, ai.batchSize));
|
|
3768
|
+
for (const batch of batches) {
|
|
3769
|
+
const system = buildSystemPrompt(batch.some((r) => r.plural !== void 0));
|
|
3770
|
+
inputTokens2 += estimateTokens(system) + estimateTokens(buildBatchPrompt(batch));
|
|
3771
|
+
for (const r of batch) outputTokens2 += estimateOutputTokens(r);
|
|
3772
|
+
}
|
|
3773
|
+
perLocale.push({ locale, requests: group.length, batches: batches.length, inputTokens: inputTokens2, outputTokens: outputTokens2 });
|
|
3774
|
+
}
|
|
3775
|
+
const inputTokens = perLocale.reduce((n, l) => n + l.inputTokens, 0);
|
|
3776
|
+
const outputTokens = perLocale.reduce((n, l) => n + l.outputTokens, 0);
|
|
3777
|
+
const pricing = resolvePricing(ai);
|
|
3778
|
+
return {
|
|
3328
3779
|
requests: reqs.length,
|
|
3329
3780
|
batches: perLocale.reduce((n, l) => n + l.batches, 0),
|
|
3330
3781
|
perLocale,
|
|
@@ -3349,45 +3800,21 @@ var init_estimate = __esm({
|
|
|
3349
3800
|
}
|
|
3350
3801
|
});
|
|
3351
3802
|
|
|
3352
|
-
// src/server/log.ts
|
|
3353
|
-
import { appendFileSync, readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
3354
|
-
import { resolve as resolve5 } from "path";
|
|
3355
|
-
function logPath(projectRoot) {
|
|
3356
|
-
return resolve5(projectRoot, ".glotfile", "log.jsonl");
|
|
3357
|
-
}
|
|
3358
|
-
function appendLog(projectRoot, entry) {
|
|
3359
|
-
ensureGlotfileDir(projectRoot);
|
|
3360
|
-
appendFileSync(logPath(projectRoot), JSON.stringify(entry) + "\n", "utf8");
|
|
3361
|
-
}
|
|
3362
|
-
function readLog(projectRoot, limit = 100) {
|
|
3363
|
-
const path = logPath(projectRoot);
|
|
3364
|
-
if (!existsSync7(path)) return [];
|
|
3365
|
-
const lines = readFileSync7(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
3366
|
-
const entries = lines.map((l) => JSON.parse(l));
|
|
3367
|
-
return entries.reverse().slice(0, limit);
|
|
3368
|
-
}
|
|
3369
|
-
var init_log = __esm({
|
|
3370
|
-
"src/server/log.ts"() {
|
|
3371
|
-
"use strict";
|
|
3372
|
-
init_glotfile_dir();
|
|
3373
|
-
}
|
|
3374
|
-
});
|
|
3375
|
-
|
|
3376
3803
|
// src/server/scan.ts
|
|
3377
|
-
import { existsSync as
|
|
3378
|
-
import { resolve as
|
|
3804
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
3805
|
+
import { resolve as resolve7 } from "path";
|
|
3379
3806
|
function loadUsageCache(projectRoot) {
|
|
3380
|
-
const path =
|
|
3381
|
-
if (!
|
|
3807
|
+
const path = resolve7(projectRoot, ".glotfile", "usage.json");
|
|
3808
|
+
if (!existsSync10(path)) return null;
|
|
3382
3809
|
try {
|
|
3383
|
-
return JSON.parse(
|
|
3810
|
+
return JSON.parse(readFileSync10(path, "utf8"));
|
|
3384
3811
|
} catch {
|
|
3385
3812
|
return null;
|
|
3386
3813
|
}
|
|
3387
3814
|
}
|
|
3388
3815
|
function saveUsageCache(projectRoot, cache2) {
|
|
3389
3816
|
ensureGlotfileDir(projectRoot);
|
|
3390
|
-
const path =
|
|
3817
|
+
const path = resolve7(projectRoot, ".glotfile", "usage.json");
|
|
3391
3818
|
writeFileAtomic(path, JSON.stringify(cache2, null, 2) + "\n");
|
|
3392
3819
|
}
|
|
3393
3820
|
function findMissing(state) {
|
|
@@ -3445,8 +3872,8 @@ var init_scan = __esm({
|
|
|
3445
3872
|
});
|
|
3446
3873
|
|
|
3447
3874
|
// src/server/scanner.ts
|
|
3448
|
-
import { readdirSync as readdirSync3, statSync as statSync2, readFileSync as
|
|
3449
|
-
import { join as
|
|
3875
|
+
import { readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync11 } from "fs";
|
|
3876
|
+
import { join as join5, extname as extname2, relative } from "path";
|
|
3450
3877
|
function scannerForExt(ext) {
|
|
3451
3878
|
return EXT_SCANNER[ext] ?? null;
|
|
3452
3879
|
}
|
|
@@ -3598,7 +4025,7 @@ function* walkFiles(dir, root, exclude) {
|
|
|
3598
4025
|
}
|
|
3599
4026
|
for (const name of entries) {
|
|
3600
4027
|
if (ALWAYS_EXCLUDE.has(name)) continue;
|
|
3601
|
-
const abs =
|
|
4028
|
+
const abs = join5(dir, name);
|
|
3602
4029
|
const rel = relative(root, abs);
|
|
3603
4030
|
let st;
|
|
3604
4031
|
try {
|
|
@@ -3628,7 +4055,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
3628
4055
|
const ext = extname2(relPath);
|
|
3629
4056
|
const scanner = scannerForExt(ext);
|
|
3630
4057
|
if (!scanner) continue;
|
|
3631
|
-
const abs =
|
|
4058
|
+
const abs = join5(projectRoot, relPath);
|
|
3632
4059
|
let st;
|
|
3633
4060
|
try {
|
|
3634
4061
|
st = statSync2(abs);
|
|
@@ -3644,279 +4071,118 @@ function runScan(projectRoot, opts, existing) {
|
|
|
3644
4071
|
}
|
|
3645
4072
|
let content;
|
|
3646
4073
|
try {
|
|
3647
|
-
content =
|
|
4074
|
+
content = readFileSync11(abs, "utf8");
|
|
3648
4075
|
} catch {
|
|
3649
|
-
continue;
|
|
3650
|
-
}
|
|
3651
|
-
cache2.files[relPath] = {
|
|
3652
|
-
mtime,
|
|
3653
|
-
size,
|
|
3654
|
-
refs: extractRefs(content, scanner, opts),
|
|
3655
|
-
prefixes: extractPrefixes(content, scanner),
|
|
3656
|
-
literals: extractLiterals(content)
|
|
3657
|
-
};
|
|
3658
|
-
}
|
|
3659
|
-
saveUsageCache(projectRoot, cache2);
|
|
3660
|
-
return cache2;
|
|
3661
|
-
}
|
|
3662
|
-
var PATTERNS, PREFIX_PATTERNS, CACHE_VERSION, EXT_SCANNER, ALWAYS_EXCLUDE, FLUTTER_ACCESSOR_DEFAULTS, KEY_SHAPE, STRING_LITERALS;
|
|
3663
|
-
var init_scanner = __esm({
|
|
3664
|
-
"src/server/scanner.ts"() {
|
|
3665
|
-
"use strict";
|
|
3666
|
-
init_scan();
|
|
3667
|
-
PATTERNS = {
|
|
3668
|
-
laravel: [
|
|
3669
|
-
/\b(?:__|trans|trans_choice|Lang::(?:get|choice))\s*\(\s*'([^']+)'/g,
|
|
3670
|
-
/\b(?:__|trans|trans_choice|Lang::(?:get|choice))\s*\(\s*"([^"]+)"/g,
|
|
3671
|
-
/@(?:lang|choice)\s*\(\s*'([^']+)'/g,
|
|
3672
|
-
/@(?:lang|choice)\s*\(\s*"([^"]+)"/g
|
|
3673
|
-
],
|
|
3674
|
-
"js-i18n": [
|
|
3675
|
-
/\$t\s*\(\s*'([^']+)'/g,
|
|
3676
|
-
/\$t\s*\(\s*"([^"]+)"/g,
|
|
3677
|
-
/\$t\s*\(\s*`([^`$\n]+)`/g,
|
|
3678
|
-
/\bi18n\.t\s*\(\s*'([^']+)'/g,
|
|
3679
|
-
/\bi18n\.t\s*\(\s*"([^"]+)"/g,
|
|
3680
|
-
/\bi18next\.t\s*\(\s*'([^']+)'/g,
|
|
3681
|
-
/\bi18next\.t\s*\(\s*"([^"]+)"/g,
|
|
3682
|
-
// t('key') — word boundary before t, not preceded by dot (excludes i18n.t which is above)
|
|
3683
|
-
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*'([^']+)'/g,
|
|
3684
|
-
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*"([^"]+)"/g,
|
|
3685
|
-
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$\n]+)`/g
|
|
3686
|
-
],
|
|
3687
|
-
gettext: [
|
|
3688
|
-
/\b(?:gettext|ngettext)\s*\(\s*'([^']+)'/g,
|
|
3689
|
-
/\b(?:gettext|ngettext)\s*\(\s*"([^"]+)"/g,
|
|
3690
|
-
// _() — word boundary, not preceded by alphanumeric
|
|
3691
|
-
/(?<![a-zA-Z0-9_$])_\s*\(\s*'([^']+)'/g,
|
|
3692
|
-
/(?<![a-zA-Z0-9_$])_\s*\(\s*"([^"]+)"/g
|
|
3693
|
-
],
|
|
3694
|
-
apple: [
|
|
3695
|
-
/NSLocalizedString\s*\(\s*@?"([^"]+)"/g,
|
|
3696
|
-
/String\s*\(\s*localized:\s*"([^"]+)"/g,
|
|
3697
|
-
/localizedString\s*\(\s*forKey:\s*"([^"]+)"/g,
|
|
3698
|
-
// The "key".localized / "key".localised String-extension idiom, where the
|
|
3699
|
-
// literal IS the key (common when keys are natural-language source text).
|
|
3700
|
-
/"([^"]+)"\s*\.\s*localized\b/g,
|
|
3701
|
-
/"([^"]+)"\s*\.\s*localised\b/g
|
|
3702
|
-
]
|
|
3703
|
-
};
|
|
3704
|
-
PREFIX_PATTERNS = {
|
|
3705
|
-
laravel: [
|
|
3706
|
-
/\b(?:__|trans|trans_choice|Lang::(?:get|choice))\s*\(\s*'([^']*)'\s*\./g,
|
|
3707
|
-
/\b(?:__|trans|trans_choice|Lang::(?:get|choice))\s*\(\s*"([^"]*)"\s*\./g,
|
|
3708
|
-
/\b(?:__|trans|trans_choice|Lang::(?:get|choice))\s*\(\s*"([^"${]*)\{?\$/g
|
|
3709
|
-
],
|
|
3710
|
-
"js-i18n": [
|
|
3711
|
-
/(?:\$t|i18n\.t|i18next\.t)\s*\(\s*'([^']*)'\s*\+/g,
|
|
3712
|
-
/(?:\$t|i18n\.t|i18next\.t)\s*\(\s*"([^"]*)"\s*\+/g,
|
|
3713
|
-
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*'([^']*)'\s*\+/g,
|
|
3714
|
-
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*"([^"]*)"\s*\+/g,
|
|
3715
|
-
/(?:\$t|i18n\.t|i18next\.t)\s*\(\s*`([^`$]*)\$\{/g,
|
|
3716
|
-
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$]*)\$\{/g
|
|
3717
|
-
]
|
|
3718
|
-
};
|
|
3719
|
-
CACHE_VERSION = 6;
|
|
3720
|
-
EXT_SCANNER = {
|
|
3721
|
-
".php": "laravel",
|
|
3722
|
-
".vue": "js-i18n",
|
|
3723
|
-
".js": "js-i18n",
|
|
3724
|
-
".ts": "js-i18n",
|
|
3725
|
-
".jsx": "js-i18n",
|
|
3726
|
-
".tsx": "js-i18n",
|
|
3727
|
-
".mjs": "js-i18n",
|
|
3728
|
-
".cjs": "js-i18n",
|
|
3729
|
-
".dart": "flutter",
|
|
3730
|
-
".py": "gettext",
|
|
3731
|
-
".c": "gettext",
|
|
3732
|
-
".cpp": "gettext",
|
|
3733
|
-
".h": "gettext",
|
|
3734
|
-
".swift": "apple",
|
|
3735
|
-
".m": "apple",
|
|
3736
|
-
".mm": "apple"
|
|
3737
|
-
};
|
|
3738
|
-
ALWAYS_EXCLUDE = /* @__PURE__ */ new Set([
|
|
3739
|
-
"node_modules",
|
|
3740
|
-
".git",
|
|
3741
|
-
".glotfile",
|
|
3742
|
-
".claude",
|
|
3743
|
-
"dist",
|
|
3744
|
-
"build",
|
|
3745
|
-
"vendor",
|
|
3746
|
-
"coverage",
|
|
3747
|
-
".next",
|
|
3748
|
-
".nuxt",
|
|
3749
|
-
".turbo",
|
|
3750
|
-
"__pycache__"
|
|
3751
|
-
]);
|
|
3752
|
-
FLUTTER_ACCESSOR_DEFAULTS = ["l10n", "loc", "localizations", "translations"];
|
|
3753
|
-
KEY_SHAPE = /^[A-Za-z0-9_][A-Za-z0-9_/-]*(?:\.(?:[A-Za-z0-9_-]+|%[sd]))+\.?$/;
|
|
3754
|
-
STRING_LITERALS = [
|
|
3755
|
-
/'([^'\\\n]+)'/g,
|
|
3756
|
-
/"([^"\\\n]+)"/g,
|
|
3757
|
-
/`([^`\\\n]+)`/g
|
|
3758
|
-
];
|
|
3759
|
-
}
|
|
3760
|
-
});
|
|
3761
|
-
|
|
3762
|
-
// src/server/ai/context.ts
|
|
3763
|
-
import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
|
|
3764
|
-
import { resolve as resolve7 } from "path";
|
|
3765
|
-
function globToRegExp2(glob) {
|
|
3766
|
-
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3767
|
-
return new RegExp(`^${escaped}$`);
|
|
3768
|
-
}
|
|
3769
|
-
function extractSnippets(refs, projectRoot, fileCache) {
|
|
3770
|
-
const filtered = refs.filter((r) => !EXCLUDED_DIRS.some((d) => r.file.startsWith(d)));
|
|
3771
|
-
const sorted = [...filtered].sort((a, b) => a.file.length - b.file.length);
|
|
3772
|
-
const selected = sorted.slice(0, MAX_SNIPPETS);
|
|
3773
|
-
const extraRefs = filtered.length > MAX_SNIPPETS ? filtered.length - MAX_SNIPPETS : 0;
|
|
3774
|
-
const snippets = [];
|
|
3775
|
-
for (const ref of selected) {
|
|
3776
|
-
const absPath = resolve7(projectRoot, ref.file);
|
|
3777
|
-
if (!fileCache.has(ref.file)) {
|
|
3778
|
-
if (!existsSync9(absPath)) continue;
|
|
3779
|
-
const content = readFileSync10(absPath, "utf8");
|
|
3780
|
-
fileCache.set(ref.file, content.split("\n"));
|
|
3781
|
-
}
|
|
3782
|
-
const lines = fileCache.get(ref.file);
|
|
3783
|
-
const start = Math.max(0, ref.line - 1 - SNIPPET_WINDOW);
|
|
3784
|
-
const end = Math.min(lines.length, ref.line + SNIPPET_WINDOW);
|
|
3785
|
-
snippets.push({
|
|
3786
|
-
file: ref.file,
|
|
3787
|
-
startLine: start + 1,
|
|
3788
|
-
lines: lines.slice(start, end).join("\n"),
|
|
3789
|
-
scanner: ref.scanner,
|
|
3790
|
-
...snippets.length === 0 && extraRefs > 0 ? { extraRefs } : {}
|
|
3791
|
-
});
|
|
3792
|
-
}
|
|
3793
|
-
return snippets;
|
|
3794
|
-
}
|
|
3795
|
-
function buildUsageIndex(cache2) {
|
|
3796
|
-
const index = /* @__PURE__ */ new Map();
|
|
3797
|
-
for (const [file, entry] of Object.entries(cache2.files)) {
|
|
3798
|
-
for (const ref of entry.refs) {
|
|
3799
|
-
const existing = index.get(ref.key) ?? [];
|
|
3800
|
-
existing.push({ key: ref.key, file, line: ref.line, col: ref.col, scanner: ref.scanner });
|
|
3801
|
-
index.set(ref.key, existing);
|
|
3802
|
-
}
|
|
3803
|
-
}
|
|
3804
|
-
return index;
|
|
3805
|
-
}
|
|
3806
|
-
function selectContextTargets(state, opts, cache2, lastRunAt) {
|
|
3807
|
-
const cutoff = opts.all ? void 0 : opts.since ?? lastRunAt;
|
|
3808
|
-
const keyRe = opts.keyGlob ? globToRegExp2(opts.keyGlob) : null;
|
|
3809
|
-
const keySet = opts.keys ? new Set(opts.keys) : null;
|
|
3810
|
-
const usageIndex = buildUsageIndex(cache2);
|
|
3811
|
-
let candidates = [];
|
|
3812
|
-
for (const key of Object.keys(state.keys).sort()) {
|
|
3813
|
-
const entry = state.keys[key];
|
|
3814
|
-
if (entry.context && !opts.force) continue;
|
|
3815
|
-
if (keySet && !keySet.has(key)) continue;
|
|
3816
|
-
if (keyRe && !keyRe.test(key)) continue;
|
|
3817
|
-
if (cutoff) {
|
|
3818
|
-
if (!entry.createdAt) continue;
|
|
3819
|
-
if (entry.createdAt < cutoff) continue;
|
|
3820
|
-
}
|
|
3821
|
-
const source = entry.values[state.config.sourceLocale]?.value ?? "";
|
|
3822
|
-
candidates.push({ id: String(candidates.length), key, source, usageSnippets: [] });
|
|
3823
|
-
}
|
|
3824
|
-
candidates.sort((a, b) => {
|
|
3825
|
-
const ta = state.keys[a.key].createdAt ?? "";
|
|
3826
|
-
const tb = state.keys[b.key].createdAt ?? "";
|
|
3827
|
-
return tb.localeCompare(ta);
|
|
3828
|
-
});
|
|
3829
|
-
if (opts.limit !== void 0) candidates = candidates.slice(0, opts.limit);
|
|
3830
|
-
candidates.forEach((c, i) => {
|
|
3831
|
-
c.id = String(i);
|
|
3832
|
-
});
|
|
3833
|
-
return candidates;
|
|
3834
|
-
}
|
|
3835
|
-
function buildContextSystemPrompt() {
|
|
3836
|
-
return [
|
|
3837
|
-
"You are a localization context writer for a UI string catalog.",
|
|
3838
|
-
"For each translation key you are given: its dot-path name, its source string, and one or more code snippets showing where the string is referenced in the codebase.",
|
|
3839
|
-
"Your task: write a concise 1\u20132 sentence context note that describes WHERE in the UI this string appears and WHAT the user is doing at that point.",
|
|
3840
|
-
"The context is read by human translators AND by an AI translation engine. It must answer: what screen is this on, what element is this (button, label, error, etc.), and what action does it relate to?",
|
|
3841
|
-
"Rules:",
|
|
3842
|
-
"- Use the code snippets as your primary signal. Look at the component name, surrounding labels, event handlers, and variable names.",
|
|
3843
|
-
"- Do NOT restate the source string itself.",
|
|
3844
|
-
"- Do NOT say 'This string is...' \u2014 write the context as a direct description.",
|
|
3845
|
-
"- Keep it under 500 characters.",
|
|
3846
|
-
"- If no code snippets are available, infer from the key path and source value."
|
|
3847
|
-
].join("\n");
|
|
3848
|
-
}
|
|
3849
|
-
function buildContextBatchPrompt(reqs) {
|
|
3850
|
-
const items = reqs.map((r) => {
|
|
3851
|
-
const snippetText = r.usageSnippets.length > 0 ? r.usageSnippets.map((s) => {
|
|
3852
|
-
const extra = s.extraRefs ? ` (and ${s.extraRefs} more call site${s.extraRefs > 1 ? "s" : ""} not shown)` : "";
|
|
3853
|
-
return `File: ${s.file} (lines ${s.startLine}+, scanner: ${s.scanner})${extra}
|
|
3854
|
-
\`\`\`
|
|
3855
|
-
${s.lines}
|
|
3856
|
-
\`\`\``;
|
|
3857
|
-
}).join("\n\n") : "(no code references found \u2014 infer from key path and source value)";
|
|
3858
|
-
return { id: r.id, key: r.key, source: r.source, codeSnippets: snippetText };
|
|
3859
|
-
});
|
|
3860
|
-
return 'Write a context note for each key. Return JSON {"items":[{"id","context"}]}.\n' + JSON.stringify(items, null, 2);
|
|
3861
|
-
}
|
|
3862
|
-
function applyContext(state, reqs, results, clock = systemClock, force = false) {
|
|
3863
|
-
const byId = new Map(reqs.map((r) => [r.id, r]));
|
|
3864
|
-
let written = 0;
|
|
3865
|
-
const errors = [];
|
|
3866
|
-
for (const res of results) {
|
|
3867
|
-
const req = byId.get(res.id);
|
|
3868
|
-
if (!req) continue;
|
|
3869
|
-
if (res.error) {
|
|
3870
|
-
errors.push({ key: req.key, error: res.error });
|
|
3871
|
-
continue;
|
|
3872
|
-
}
|
|
3873
|
-
const context = res.context?.trim() ?? "";
|
|
3874
|
-
if (!context) {
|
|
3875
|
-
errors.push({ key: req.key, error: "AI returned empty context" });
|
|
3876
|
-
continue;
|
|
3877
|
-
}
|
|
3878
|
-
if (context.length > MAX_CONTEXT_LENGTH) {
|
|
3879
|
-
errors.push({ key: req.key, error: `Context too long (${context.length} chars, max ${MAX_CONTEXT_LENGTH})` });
|
|
3880
|
-
continue;
|
|
3881
|
-
}
|
|
3882
|
-
const entry = state.keys[req.key];
|
|
3883
|
-
if (!entry || entry.context && !force) continue;
|
|
3884
|
-
entry.context = context;
|
|
3885
|
-
entry.contextSource = "ai";
|
|
3886
|
-
entry.contextAt = clock();
|
|
3887
|
-
written++;
|
|
3888
|
-
}
|
|
3889
|
-
return { written, errors };
|
|
3890
|
-
}
|
|
3891
|
-
var MAX_CONTEXT_LENGTH, SNIPPET_WINDOW, MAX_SNIPPETS, EXCLUDED_DIRS, CONTEXT_BATCH_SCHEMA;
|
|
3892
|
-
var init_context = __esm({
|
|
3893
|
-
"src/server/ai/context.ts"() {
|
|
3894
|
-
"use strict";
|
|
3895
|
-
init_state();
|
|
3896
|
-
MAX_CONTEXT_LENGTH = 500;
|
|
3897
|
-
SNIPPET_WINDOW = 15;
|
|
3898
|
-
MAX_SNIPPETS = 3;
|
|
3899
|
-
EXCLUDED_DIRS = ["node_modules/", "vendor/", "dist/", ".git/", ".glotfile/"];
|
|
3900
|
-
CONTEXT_BATCH_SCHEMA = {
|
|
3901
|
-
type: "object",
|
|
3902
|
-
properties: {
|
|
3903
|
-
items: {
|
|
3904
|
-
type: "array",
|
|
3905
|
-
items: {
|
|
3906
|
-
type: "object",
|
|
3907
|
-
properties: {
|
|
3908
|
-
id: { type: "string" },
|
|
3909
|
-
context: { type: "string" },
|
|
3910
|
-
error: { type: "string" }
|
|
3911
|
-
},
|
|
3912
|
-
required: ["id"],
|
|
3913
|
-
additionalProperties: false
|
|
3914
|
-
}
|
|
3915
|
-
}
|
|
3916
|
-
},
|
|
3917
|
-
required: ["items"],
|
|
3918
|
-
additionalProperties: false
|
|
4076
|
+
continue;
|
|
4077
|
+
}
|
|
4078
|
+
cache2.files[relPath] = {
|
|
4079
|
+
mtime,
|
|
4080
|
+
size,
|
|
4081
|
+
refs: extractRefs(content, scanner, opts),
|
|
4082
|
+
prefixes: extractPrefixes(content, scanner),
|
|
4083
|
+
literals: extractLiterals(content)
|
|
4084
|
+
};
|
|
4085
|
+
}
|
|
4086
|
+
saveUsageCache(projectRoot, cache2);
|
|
4087
|
+
return cache2;
|
|
4088
|
+
}
|
|
4089
|
+
var PATTERNS, PREFIX_PATTERNS, CACHE_VERSION, EXT_SCANNER, ALWAYS_EXCLUDE, FLUTTER_ACCESSOR_DEFAULTS, KEY_SHAPE, STRING_LITERALS;
|
|
4090
|
+
var init_scanner = __esm({
|
|
4091
|
+
"src/server/scanner.ts"() {
|
|
4092
|
+
"use strict";
|
|
4093
|
+
init_scan();
|
|
4094
|
+
PATTERNS = {
|
|
4095
|
+
laravel: [
|
|
4096
|
+
/\b(?:__|trans|trans_choice|Lang::(?:get|choice))\s*\(\s*'([^']+)'/g,
|
|
4097
|
+
/\b(?:__|trans|trans_choice|Lang::(?:get|choice))\s*\(\s*"([^"]+)"/g,
|
|
4098
|
+
/@(?:lang|choice)\s*\(\s*'([^']+)'/g,
|
|
4099
|
+
/@(?:lang|choice)\s*\(\s*"([^"]+)"/g
|
|
4100
|
+
],
|
|
4101
|
+
"js-i18n": [
|
|
4102
|
+
/\$t\s*\(\s*'([^']+)'/g,
|
|
4103
|
+
/\$t\s*\(\s*"([^"]+)"/g,
|
|
4104
|
+
/\$t\s*\(\s*`([^`$\n]+)`/g,
|
|
4105
|
+
/\bi18n\.t\s*\(\s*'([^']+)'/g,
|
|
4106
|
+
/\bi18n\.t\s*\(\s*"([^"]+)"/g,
|
|
4107
|
+
/\bi18next\.t\s*\(\s*'([^']+)'/g,
|
|
4108
|
+
/\bi18next\.t\s*\(\s*"([^"]+)"/g,
|
|
4109
|
+
// t('key') — word boundary before t, not preceded by dot (excludes i18n.t which is above)
|
|
4110
|
+
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*'([^']+)'/g,
|
|
4111
|
+
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*"([^"]+)"/g,
|
|
4112
|
+
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$\n]+)`/g
|
|
4113
|
+
],
|
|
4114
|
+
gettext: [
|
|
4115
|
+
/\b(?:gettext|ngettext)\s*\(\s*'([^']+)'/g,
|
|
4116
|
+
/\b(?:gettext|ngettext)\s*\(\s*"([^"]+)"/g,
|
|
4117
|
+
// _() — word boundary, not preceded by alphanumeric
|
|
4118
|
+
/(?<![a-zA-Z0-9_$])_\s*\(\s*'([^']+)'/g,
|
|
4119
|
+
/(?<![a-zA-Z0-9_$])_\s*\(\s*"([^"]+)"/g
|
|
4120
|
+
],
|
|
4121
|
+
apple: [
|
|
4122
|
+
/NSLocalizedString\s*\(\s*@?"([^"]+)"/g,
|
|
4123
|
+
/String\s*\(\s*localized:\s*"([^"]+)"/g,
|
|
4124
|
+
/localizedString\s*\(\s*forKey:\s*"([^"]+)"/g,
|
|
4125
|
+
// The "key".localized / "key".localised String-extension idiom, where the
|
|
4126
|
+
// literal IS the key (common when keys are natural-language source text).
|
|
4127
|
+
/"([^"]+)"\s*\.\s*localized\b/g,
|
|
4128
|
+
/"([^"]+)"\s*\.\s*localised\b/g
|
|
4129
|
+
]
|
|
4130
|
+
};
|
|
4131
|
+
PREFIX_PATTERNS = {
|
|
4132
|
+
laravel: [
|
|
4133
|
+
/\b(?:__|trans|trans_choice|Lang::(?:get|choice))\s*\(\s*'([^']*)'\s*\./g,
|
|
4134
|
+
/\b(?:__|trans|trans_choice|Lang::(?:get|choice))\s*\(\s*"([^"]*)"\s*\./g,
|
|
4135
|
+
/\b(?:__|trans|trans_choice|Lang::(?:get|choice))\s*\(\s*"([^"${]*)\{?\$/g
|
|
4136
|
+
],
|
|
4137
|
+
"js-i18n": [
|
|
4138
|
+
/(?:\$t|i18n\.t|i18next\.t)\s*\(\s*'([^']*)'\s*\+/g,
|
|
4139
|
+
/(?:\$t|i18n\.t|i18next\.t)\s*\(\s*"([^"]*)"\s*\+/g,
|
|
4140
|
+
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*'([^']*)'\s*\+/g,
|
|
4141
|
+
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*"([^"]*)"\s*\+/g,
|
|
4142
|
+
/(?:\$t|i18n\.t|i18next\.t)\s*\(\s*`([^`$]*)\$\{/g,
|
|
4143
|
+
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$]*)\$\{/g
|
|
4144
|
+
]
|
|
4145
|
+
};
|
|
4146
|
+
CACHE_VERSION = 6;
|
|
4147
|
+
EXT_SCANNER = {
|
|
4148
|
+
".php": "laravel",
|
|
4149
|
+
".vue": "js-i18n",
|
|
4150
|
+
".js": "js-i18n",
|
|
4151
|
+
".ts": "js-i18n",
|
|
4152
|
+
".jsx": "js-i18n",
|
|
4153
|
+
".tsx": "js-i18n",
|
|
4154
|
+
".mjs": "js-i18n",
|
|
4155
|
+
".cjs": "js-i18n",
|
|
4156
|
+
".dart": "flutter",
|
|
4157
|
+
".py": "gettext",
|
|
4158
|
+
".c": "gettext",
|
|
4159
|
+
".cpp": "gettext",
|
|
4160
|
+
".h": "gettext",
|
|
4161
|
+
".swift": "apple",
|
|
4162
|
+
".m": "apple",
|
|
4163
|
+
".mm": "apple"
|
|
3919
4164
|
};
|
|
4165
|
+
ALWAYS_EXCLUDE = /* @__PURE__ */ new Set([
|
|
4166
|
+
"node_modules",
|
|
4167
|
+
".git",
|
|
4168
|
+
".glotfile",
|
|
4169
|
+
".claude",
|
|
4170
|
+
"dist",
|
|
4171
|
+
"build",
|
|
4172
|
+
"vendor",
|
|
4173
|
+
"coverage",
|
|
4174
|
+
".next",
|
|
4175
|
+
".nuxt",
|
|
4176
|
+
".turbo",
|
|
4177
|
+
"__pycache__"
|
|
4178
|
+
]);
|
|
4179
|
+
FLUTTER_ACCESSOR_DEFAULTS = ["l10n", "loc", "localizations", "translations"];
|
|
4180
|
+
KEY_SHAPE = /^[A-Za-z0-9_][A-Za-z0-9_/-]*(?:\.(?:[A-Za-z0-9_-]+|%[sd]))+\.?$/;
|
|
4181
|
+
STRING_LITERALS = [
|
|
4182
|
+
/'([^'\\\n]+)'/g,
|
|
4183
|
+
/"([^"\\\n]+)"/g,
|
|
4184
|
+
/`([^`\\\n]+)`/g
|
|
4185
|
+
];
|
|
3920
4186
|
}
|
|
3921
4187
|
});
|
|
3922
4188
|
|
|
@@ -4292,7 +4558,7 @@ var init_run2 = __esm({
|
|
|
4292
4558
|
});
|
|
4293
4559
|
|
|
4294
4560
|
// src/server/lint/outputs.ts
|
|
4295
|
-
import { readFileSync as
|
|
4561
|
+
import { readFileSync as readFileSync12, existsSync as existsSync11 } from "fs";
|
|
4296
4562
|
import { resolve as resolve8 } from "path";
|
|
4297
4563
|
function checkOutputs(state, root) {
|
|
4298
4564
|
const out = [];
|
|
@@ -4300,7 +4566,7 @@ function checkOutputs(state, root) {
|
|
|
4300
4566
|
const result = getAdapter(output.adapter).export(state, output);
|
|
4301
4567
|
for (const file of result.files) {
|
|
4302
4568
|
const abs = resolve8(root, file.path);
|
|
4303
|
-
const current =
|
|
4569
|
+
const current = existsSync11(abs) ? readFileSync12(abs, "utf8") : null;
|
|
4304
4570
|
if (current === null) {
|
|
4305
4571
|
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
4306
4572
|
} else if (current !== file.contents) {
|
|
@@ -4345,8 +4611,8 @@ var init_accept = __esm({
|
|
|
4345
4611
|
});
|
|
4346
4612
|
|
|
4347
4613
|
// src/server/import/detect.ts
|
|
4348
|
-
import { existsSync as
|
|
4349
|
-
import { join as
|
|
4614
|
+
import { existsSync as existsSync12, readdirSync as readdirSync4, readFileSync as readFileSync13, statSync as statSync3 } from "fs";
|
|
4615
|
+
import { join as join6 } from "path";
|
|
4350
4616
|
function safeIsDir(p) {
|
|
4351
4617
|
try {
|
|
4352
4618
|
return statSync3(p).isDirectory();
|
|
@@ -4355,7 +4621,7 @@ function safeIsDir(p) {
|
|
|
4355
4621
|
}
|
|
4356
4622
|
}
|
|
4357
4623
|
function listDirs(dir) {
|
|
4358
|
-
return readdirSync4(dir).filter((e) => safeIsDir(
|
|
4624
|
+
return readdirSync4(dir).filter((e) => safeIsDir(join6(dir, e)));
|
|
4359
4625
|
}
|
|
4360
4626
|
function fileCount(dir) {
|
|
4361
4627
|
try {
|
|
@@ -4369,23 +4635,23 @@ function pickSource(locales, sizeOf) {
|
|
|
4369
4635
|
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
4370
4636
|
}
|
|
4371
4637
|
function detectLaravel(root) {
|
|
4372
|
-
const localeRoot = [
|
|
4638
|
+
const localeRoot = [join6(root, "resources", "lang"), join6(root, "lang")].find(safeIsDir);
|
|
4373
4639
|
if (!localeRoot) return null;
|
|
4374
4640
|
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
4375
4641
|
if (locales.length === 0) return null;
|
|
4376
|
-
const sourceLocale = pickSource(locales, (loc) => fileCount(
|
|
4642
|
+
const sourceLocale = pickSource(locales, (loc) => fileCount(join6(localeRoot, loc)));
|
|
4377
4643
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
4378
4644
|
}
|
|
4379
4645
|
function detectVue(root, forced = false) {
|
|
4380
4646
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
4381
|
-
const localeRoot =
|
|
4647
|
+
const localeRoot = join6(root, rel);
|
|
4382
4648
|
if (!safeIsDir(localeRoot)) continue;
|
|
4383
4649
|
const locales = readdirSync4(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4384
4650
|
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
4385
4651
|
if (enough) {
|
|
4386
4652
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4387
4653
|
try {
|
|
4388
|
-
return statSync3(
|
|
4654
|
+
return statSync3(join6(localeRoot, `${loc}.json`)).size;
|
|
4389
4655
|
} catch {
|
|
4390
4656
|
return 0;
|
|
4391
4657
|
}
|
|
@@ -4397,7 +4663,7 @@ function detectVue(root, forced = false) {
|
|
|
4397
4663
|
}
|
|
4398
4664
|
function detectArb(root) {
|
|
4399
4665
|
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
4400
|
-
const localeRoot =
|
|
4666
|
+
const localeRoot = join6(root, rel);
|
|
4401
4667
|
if (!safeIsDir(localeRoot)) continue;
|
|
4402
4668
|
const locales = readdirSync4(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
4403
4669
|
if (locales.length >= 1) {
|
|
@@ -4407,10 +4673,10 @@ function detectArb(root) {
|
|
|
4407
4673
|
return null;
|
|
4408
4674
|
}
|
|
4409
4675
|
function lprojLocales(dir) {
|
|
4410
|
-
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) &&
|
|
4676
|
+
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync12(join6(dir, `${l}.lproj`, "Localizable.strings")));
|
|
4411
4677
|
}
|
|
4412
4678
|
function detectApple(root) {
|
|
4413
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
4679
|
+
const candidates = [root, ...listDirs(root).map((d) => join6(root, d))];
|
|
4414
4680
|
let best = null;
|
|
4415
4681
|
for (const dir of candidates) {
|
|
4416
4682
|
const locales = lprojLocales(dir);
|
|
@@ -4422,7 +4688,7 @@ function detectApple(root) {
|
|
|
4422
4688
|
locales,
|
|
4423
4689
|
sourceLocale: pickSource(locales, (loc) => {
|
|
4424
4690
|
try {
|
|
4425
|
-
return statSync3(
|
|
4691
|
+
return statSync3(join6(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
4426
4692
|
} catch {
|
|
4427
4693
|
return 0;
|
|
4428
4694
|
}
|
|
@@ -4434,7 +4700,7 @@ function detectApple(root) {
|
|
|
4434
4700
|
}
|
|
4435
4701
|
function detectAngularXliff(root) {
|
|
4436
4702
|
for (const rel of ANGULAR_DIR_CANDIDATES) {
|
|
4437
|
-
const localeRoot = rel === "." ? root :
|
|
4703
|
+
const localeRoot = rel === "." ? root : join6(root, rel);
|
|
4438
4704
|
if (!safeIsDir(localeRoot)) continue;
|
|
4439
4705
|
const files = readdirSync4(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
|
|
4440
4706
|
if (files.length === 0) continue;
|
|
@@ -4442,7 +4708,7 @@ function detectAngularXliff(root) {
|
|
|
4442
4708
|
const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
|
|
4443
4709
|
let sourceLocale;
|
|
4444
4710
|
try {
|
|
4445
|
-
sourceLocale =
|
|
4711
|
+
sourceLocale = readFileSync13(join6(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
|
|
4446
4712
|
} catch {
|
|
4447
4713
|
}
|
|
4448
4714
|
if (!sourceLocale && locales.length === 0) continue;
|
|
@@ -4453,14 +4719,14 @@ function detectAngularXliff(root) {
|
|
|
4453
4719
|
return null;
|
|
4454
4720
|
}
|
|
4455
4721
|
function detectRails(root) {
|
|
4456
|
-
const localeRoot =
|
|
4722
|
+
const localeRoot = join6(root, "config", "locales");
|
|
4457
4723
|
if (!safeIsDir(localeRoot)) return null;
|
|
4458
4724
|
const locales = [];
|
|
4459
4725
|
for (const file of readdirSync4(localeRoot).sort()) {
|
|
4460
4726
|
if (!/\.ya?ml$/.test(file)) continue;
|
|
4461
4727
|
let text;
|
|
4462
4728
|
try {
|
|
4463
|
-
text =
|
|
4729
|
+
text = readFileSync13(join6(localeRoot, file), "utf8");
|
|
4464
4730
|
} catch {
|
|
4465
4731
|
continue;
|
|
4466
4732
|
}
|
|
@@ -4474,15 +4740,15 @@ function detectRails(root) {
|
|
|
4474
4740
|
}
|
|
4475
4741
|
function detectI18next(root) {
|
|
4476
4742
|
for (const rel of I18NEXT_DIR_CANDIDATES) {
|
|
4477
|
-
const localeRoot =
|
|
4743
|
+
const localeRoot = join6(root, rel);
|
|
4478
4744
|
if (!safeIsDir(localeRoot)) continue;
|
|
4479
4745
|
const locales = listDirs(localeRoot).filter(
|
|
4480
|
-
(d) => LOCALE_RE.test(d) && readdirSync4(
|
|
4746
|
+
(d) => LOCALE_RE.test(d) && readdirSync4(join6(localeRoot, d)).some((f) => f.endsWith(".json"))
|
|
4481
4747
|
);
|
|
4482
4748
|
if (locales.length === 0) continue;
|
|
4483
4749
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4484
4750
|
try {
|
|
4485
|
-
return readdirSync4(
|
|
4751
|
+
return readdirSync4(join6(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync3(join6(localeRoot, loc, f)).size, 0);
|
|
4486
4752
|
} catch {
|
|
4487
4753
|
return 0;
|
|
4488
4754
|
}
|
|
@@ -4499,8 +4765,8 @@ function gettextLocales(dir) {
|
|
|
4499
4765
|
if (!locales.includes(flat)) locales.push(flat);
|
|
4500
4766
|
continue;
|
|
4501
4767
|
}
|
|
4502
|
-
if (!LOCALE_RE.test(entry) || !safeIsDir(
|
|
4503
|
-
const sub =
|
|
4768
|
+
if (!LOCALE_RE.test(entry) || !safeIsDir(join6(dir, entry))) continue;
|
|
4769
|
+
const sub = join6(dir, entry);
|
|
4504
4770
|
const hasPo = (d) => {
|
|
4505
4771
|
try {
|
|
4506
4772
|
return readdirSync4(d).some((f) => f.endsWith(".po"));
|
|
@@ -4508,7 +4774,7 @@ function gettextLocales(dir) {
|
|
|
4508
4774
|
return false;
|
|
4509
4775
|
}
|
|
4510
4776
|
};
|
|
4511
|
-
if (hasPo(
|
|
4777
|
+
if (hasPo(join6(sub, "LC_MESSAGES")) || hasPo(sub)) {
|
|
4512
4778
|
if (!locales.includes(entry)) locales.push(entry);
|
|
4513
4779
|
}
|
|
4514
4780
|
}
|
|
@@ -4516,7 +4782,7 @@ function gettextLocales(dir) {
|
|
|
4516
4782
|
}
|
|
4517
4783
|
function detectGettext(root) {
|
|
4518
4784
|
for (const rel of GETTEXT_DIR_CANDIDATES) {
|
|
4519
|
-
const localeRoot =
|
|
4785
|
+
const localeRoot = join6(root, rel);
|
|
4520
4786
|
if (!safeIsDir(localeRoot)) continue;
|
|
4521
4787
|
const locales = gettextLocales(localeRoot);
|
|
4522
4788
|
if (locales.length === 0) continue;
|
|
@@ -4525,10 +4791,10 @@ function detectGettext(root) {
|
|
|
4525
4791
|
return null;
|
|
4526
4792
|
}
|
|
4527
4793
|
function detectAppleStringsdict(root) {
|
|
4528
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
4794
|
+
const candidates = [root, ...listDirs(root).map((d) => join6(root, d))];
|
|
4529
4795
|
let best = null;
|
|
4530
4796
|
for (const dir of candidates) {
|
|
4531
|
-
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) &&
|
|
4797
|
+
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync12(join6(dir, `${l}.lproj`, "Localizable.stringsdict")));
|
|
4532
4798
|
if (locales.length === 0) continue;
|
|
4533
4799
|
if (!best || locales.length > best.locales.length) {
|
|
4534
4800
|
best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
@@ -4537,7 +4803,7 @@ function detectAppleStringsdict(root) {
|
|
|
4537
4803
|
return best;
|
|
4538
4804
|
}
|
|
4539
4805
|
function detect(root, formatOverride) {
|
|
4540
|
-
if (!
|
|
4806
|
+
if (!existsSync12(root)) return null;
|
|
4541
4807
|
if (formatOverride) {
|
|
4542
4808
|
const fn = BY_FORMAT[formatOverride];
|
|
4543
4809
|
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
@@ -4611,8 +4877,8 @@ var init_flatten = __esm({
|
|
|
4611
4877
|
});
|
|
4612
4878
|
|
|
4613
4879
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
4614
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
4615
|
-
import { join as
|
|
4880
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync14 } from "fs";
|
|
4881
|
+
import { join as join7 } from "path";
|
|
4616
4882
|
var LOCALE_RE2, vueI18nJson2;
|
|
4617
4883
|
var init_vue_i18n_json2 = __esm({
|
|
4618
4884
|
"src/server/import/parsers/vue-i18n-json.ts"() {
|
|
@@ -4632,7 +4898,7 @@ var init_vue_i18n_json2 = __esm({
|
|
|
4632
4898
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4633
4899
|
let data;
|
|
4634
4900
|
try {
|
|
4635
|
-
data = JSON.parse(
|
|
4901
|
+
data = JSON.parse(readFileSync14(join7(localeRoot, file), "utf8"));
|
|
4636
4902
|
} catch (e) {
|
|
4637
4903
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
4638
4904
|
continue;
|
|
@@ -4660,16 +4926,16 @@ var init_placeholders2 = __esm({
|
|
|
4660
4926
|
|
|
4661
4927
|
// src/server/import/parsers/laravel-php.ts
|
|
4662
4928
|
import { readdirSync as readdirSync6, statSync as statSync4 } from "fs";
|
|
4663
|
-
import { join as
|
|
4929
|
+
import { join as join8, relative as relative2 } from "path";
|
|
4664
4930
|
import { execFileSync } from "child_process";
|
|
4665
4931
|
function listDirs2(dir) {
|
|
4666
|
-
return readdirSync6(dir).filter((e) => statSync4(
|
|
4932
|
+
return readdirSync6(dir).filter((e) => statSync4(join8(dir, e)).isDirectory());
|
|
4667
4933
|
}
|
|
4668
4934
|
function listPhpFiles(dir) {
|
|
4669
4935
|
const out = [];
|
|
4670
4936
|
const walk = (d) => {
|
|
4671
4937
|
for (const e of readdirSync6(d)) {
|
|
4672
|
-
const full =
|
|
4938
|
+
const full = join8(d, e);
|
|
4673
4939
|
if (statSync4(full).isDirectory()) walk(full);
|
|
4674
4940
|
else if (e.endsWith(".php")) out.push(full);
|
|
4675
4941
|
}
|
|
@@ -4712,7 +4978,7 @@ var init_laravel_php2 = __esm({
|
|
|
4712
4978
|
for (const locale of listDirs2(localeRoot).sort()) {
|
|
4713
4979
|
if (locale === "vendor") continue;
|
|
4714
4980
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4715
|
-
const localeDir =
|
|
4981
|
+
const localeDir = join8(localeRoot, locale);
|
|
4716
4982
|
locales.push(locale);
|
|
4717
4983
|
for (const file of listPhpFiles(localeDir)) {
|
|
4718
4984
|
const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
|
|
@@ -4737,8 +5003,8 @@ var init_laravel_php2 = __esm({
|
|
|
4737
5003
|
});
|
|
4738
5004
|
|
|
4739
5005
|
// src/server/import/parsers/flutter-arb.ts
|
|
4740
|
-
import { readdirSync as readdirSync7, readFileSync as
|
|
4741
|
-
import { join as
|
|
5006
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync15 } from "fs";
|
|
5007
|
+
import { join as join9 } from "path";
|
|
4742
5008
|
function localeFromArbName(file) {
|
|
4743
5009
|
const m = file.match(/^(.+)\.arb$/);
|
|
4744
5010
|
if (!m) return null;
|
|
@@ -4778,7 +5044,7 @@ var init_flutter_arb2 = __esm({
|
|
|
4778
5044
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4779
5045
|
let data;
|
|
4780
5046
|
try {
|
|
4781
|
-
data = JSON.parse(
|
|
5047
|
+
data = JSON.parse(readFileSync15(join9(localeRoot, file), "utf8"));
|
|
4782
5048
|
} catch (e) {
|
|
4783
5049
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
4784
5050
|
continue;
|
|
@@ -4805,8 +5071,8 @@ var init_flutter_arb2 = __esm({
|
|
|
4805
5071
|
});
|
|
4806
5072
|
|
|
4807
5073
|
// src/server/import/parsers/apple-strings.ts
|
|
4808
|
-
import { readdirSync as readdirSync8, readFileSync as
|
|
4809
|
-
import { join as
|
|
5074
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync16, statSync as statSync5 } from "fs";
|
|
5075
|
+
import { join as join10 } from "path";
|
|
4810
5076
|
function localeFromLproj(dir) {
|
|
4811
5077
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
4812
5078
|
if (!m) return null;
|
|
@@ -4914,16 +5180,16 @@ var init_apple_strings2 = __esm({
|
|
|
4914
5180
|
const locale = localeFromLproj(dir);
|
|
4915
5181
|
if (!locale) continue;
|
|
4916
5182
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4917
|
-
const file =
|
|
5183
|
+
const file = join10(localeRoot, dir, TABLE);
|
|
4918
5184
|
let text;
|
|
4919
5185
|
try {
|
|
4920
5186
|
if (!statSync5(file).isFile()) continue;
|
|
4921
|
-
text =
|
|
5187
|
+
text = readFileSync16(file, "utf8");
|
|
4922
5188
|
} catch {
|
|
4923
5189
|
continue;
|
|
4924
5190
|
}
|
|
4925
5191
|
locales.push(locale);
|
|
4926
|
-
const others = readdirSync8(
|
|
5192
|
+
const others = readdirSync8(join10(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
|
|
4927
5193
|
if (others.length) {
|
|
4928
5194
|
warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
|
|
4929
5195
|
}
|
|
@@ -4938,8 +5204,8 @@ var init_apple_strings2 = __esm({
|
|
|
4938
5204
|
});
|
|
4939
5205
|
|
|
4940
5206
|
// src/server/import/parsers/angular-xliff.ts
|
|
4941
|
-
import { readdirSync as readdirSync9, readFileSync as
|
|
4942
|
-
import { join as
|
|
5207
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync17 } from "fs";
|
|
5208
|
+
import { join as join11 } from "path";
|
|
4943
5209
|
function decodeEntities(s) {
|
|
4944
5210
|
return s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))).replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
4945
5211
|
}
|
|
@@ -4991,7 +5257,7 @@ var init_angular_xliff2 = __esm({
|
|
|
4991
5257
|
if (fnameLocale !== void 0 && !LOCALE_RE5.test(fnameLocale)) continue;
|
|
4992
5258
|
let xml;
|
|
4993
5259
|
try {
|
|
4994
|
-
xml =
|
|
5260
|
+
xml = readFileSync17(join11(localeRoot, file), "utf8");
|
|
4995
5261
|
} catch (e) {
|
|
4996
5262
|
warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
|
|
4997
5263
|
continue;
|
|
@@ -5034,8 +5300,8 @@ var init_angular_xliff2 = __esm({
|
|
|
5034
5300
|
});
|
|
5035
5301
|
|
|
5036
5302
|
// src/server/import/parsers/gettext-po.ts
|
|
5037
|
-
import { readdirSync as readdirSync10, readFileSync as
|
|
5038
|
-
import { join as
|
|
5303
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync18 } from "fs";
|
|
5304
|
+
import { join as join12 } from "path";
|
|
5039
5305
|
function unescapePo(s) {
|
|
5040
5306
|
return s.replace(
|
|
5041
5307
|
/\\([\\"ntr])/g,
|
|
@@ -5106,17 +5372,17 @@ function discoverPoFiles(root) {
|
|
|
5106
5372
|
for (const e of entries) {
|
|
5107
5373
|
if (e.isFile() && e.name.endsWith(".po")) {
|
|
5108
5374
|
const base = e.name.slice(0, -3);
|
|
5109
|
-
found.push({ path:
|
|
5375
|
+
found.push({ path: join12(root, e.name), rel: e.name, locale: LOCALE_RE6.test(base) ? base : null });
|
|
5110
5376
|
} else if (e.isDirectory() && LOCALE_RE6.test(e.name)) {
|
|
5111
|
-
for (const sub of [
|
|
5377
|
+
for (const sub of [join12(e.name, "LC_MESSAGES"), e.name]) {
|
|
5112
5378
|
let names;
|
|
5113
5379
|
try {
|
|
5114
|
-
names = readdirSync10(
|
|
5380
|
+
names = readdirSync10(join12(root, sub)).sort();
|
|
5115
5381
|
} catch {
|
|
5116
5382
|
continue;
|
|
5117
5383
|
}
|
|
5118
5384
|
for (const f of names) {
|
|
5119
|
-
if (f.endsWith(".po")) found.push({ path:
|
|
5385
|
+
if (f.endsWith(".po")) found.push({ path: join12(root, sub, f), rel: join12(sub, f), locale: e.name });
|
|
5120
5386
|
}
|
|
5121
5387
|
}
|
|
5122
5388
|
}
|
|
@@ -5140,7 +5406,7 @@ var init_gettext_po2 = __esm({
|
|
|
5140
5406
|
for (const file of discoverPoFiles(localeRoot)) {
|
|
5141
5407
|
let entries;
|
|
5142
5408
|
try {
|
|
5143
|
-
entries = parseEntries(
|
|
5409
|
+
entries = parseEntries(readFileSync18(file.path, "utf8"));
|
|
5144
5410
|
} catch (e) {
|
|
5145
5411
|
warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
|
|
5146
5412
|
continue;
|
|
@@ -5187,8 +5453,8 @@ var init_gettext_po2 = __esm({
|
|
|
5187
5453
|
});
|
|
5188
5454
|
|
|
5189
5455
|
// src/server/import/parsers/i18next-json.ts
|
|
5190
|
-
import { readdirSync as readdirSync11, readFileSync as
|
|
5191
|
-
import { join as
|
|
5456
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync19, statSync as statSync6 } from "fs";
|
|
5457
|
+
import { join as join13 } from "path";
|
|
5192
5458
|
function safeIsDir2(p) {
|
|
5193
5459
|
try {
|
|
5194
5460
|
return statSync6(p).isDirectory();
|
|
@@ -5203,7 +5469,7 @@ function fromI18next(value) {
|
|
|
5203
5469
|
function ingestFile(path, label, prefix, locale, keys, warnings) {
|
|
5204
5470
|
let data;
|
|
5205
5471
|
try {
|
|
5206
|
-
data = JSON.parse(
|
|
5472
|
+
data = JSON.parse(readFileSync19(path, "utf8"));
|
|
5207
5473
|
} catch (e) {
|
|
5208
5474
|
warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
|
|
5209
5475
|
return false;
|
|
@@ -5256,7 +5522,7 @@ var init_i18next_json2 = __esm({
|
|
|
5256
5522
|
const keys = {};
|
|
5257
5523
|
const locales = [];
|
|
5258
5524
|
for (const entry of readdirSync11(localeRoot).sort()) {
|
|
5259
|
-
const full =
|
|
5525
|
+
const full = join13(localeRoot, entry);
|
|
5260
5526
|
if (safeIsDir2(full)) {
|
|
5261
5527
|
if (!LOCALE_RE7.test(entry)) continue;
|
|
5262
5528
|
if (opts?.locales && !opts.locales.includes(entry)) continue;
|
|
@@ -5265,7 +5531,7 @@ var init_i18next_json2 = __esm({
|
|
|
5265
5531
|
if (!file.endsWith(".json")) continue;
|
|
5266
5532
|
const ns = file.slice(0, -".json".length);
|
|
5267
5533
|
const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
|
|
5268
|
-
if (ingestFile(
|
|
5534
|
+
if (ingestFile(join13(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
|
|
5269
5535
|
}
|
|
5270
5536
|
if (any && !locales.includes(entry)) locales.push(entry);
|
|
5271
5537
|
} else if (entry.endsWith(".json")) {
|
|
@@ -5284,8 +5550,8 @@ var init_i18next_json2 = __esm({
|
|
|
5284
5550
|
});
|
|
5285
5551
|
|
|
5286
5552
|
// src/server/import/parsers/rails-yaml.ts
|
|
5287
|
-
import { readdirSync as readdirSync12, readFileSync as
|
|
5288
|
-
import { join as
|
|
5553
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync20 } from "fs";
|
|
5554
|
+
import { join as join14 } from "path";
|
|
5289
5555
|
function fromRuby(value) {
|
|
5290
5556
|
return value.replace(/%\{(\w+)\}/g, "{$1}");
|
|
5291
5557
|
}
|
|
@@ -5502,7 +5768,7 @@ var init_rails_yaml2 = __esm({
|
|
|
5502
5768
|
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
5503
5769
|
let text;
|
|
5504
5770
|
try {
|
|
5505
|
-
text =
|
|
5771
|
+
text = readFileSync20(join14(localeRoot, file), "utf8");
|
|
5506
5772
|
} catch (e) {
|
|
5507
5773
|
warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
|
|
5508
5774
|
continue;
|
|
@@ -5525,8 +5791,8 @@ var init_rails_yaml2 = __esm({
|
|
|
5525
5791
|
});
|
|
5526
5792
|
|
|
5527
5793
|
// src/server/import/parsers/apple-stringsdict.ts
|
|
5528
|
-
import { readdirSync as readdirSync13, readFileSync as
|
|
5529
|
-
import { join as
|
|
5794
|
+
import { readdirSync as readdirSync13, readFileSync as readFileSync21, statSync as statSync7 } from "fs";
|
|
5795
|
+
import { join as join15 } from "path";
|
|
5530
5796
|
function localeFromLproj2(dir) {
|
|
5531
5797
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
5532
5798
|
if (!m) return null;
|
|
@@ -5668,16 +5934,16 @@ var init_apple_stringsdict2 = __esm({
|
|
|
5668
5934
|
const locale = localeFromLproj2(dir);
|
|
5669
5935
|
if (!locale) continue;
|
|
5670
5936
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5671
|
-
const file =
|
|
5937
|
+
const file = join15(localeRoot, dir, TABLE2);
|
|
5672
5938
|
let text;
|
|
5673
5939
|
try {
|
|
5674
5940
|
if (!statSync7(file).isFile()) continue;
|
|
5675
|
-
text =
|
|
5941
|
+
text = readFileSync21(file, "utf8");
|
|
5676
5942
|
} catch {
|
|
5677
5943
|
continue;
|
|
5678
5944
|
}
|
|
5679
5945
|
locales.push(locale);
|
|
5680
|
-
const others = readdirSync13(
|
|
5946
|
+
const others = readdirSync13(join15(localeRoot, dir)).filter(
|
|
5681
5947
|
(f) => f.endsWith(".stringsdict") && f !== TABLE2
|
|
5682
5948
|
);
|
|
5683
5949
|
if (others.length) {
|
|
@@ -6136,12 +6402,12 @@ var init_checks = __esm({
|
|
|
6136
6402
|
});
|
|
6137
6403
|
|
|
6138
6404
|
// src/server/ui-prefs.ts
|
|
6139
|
-
import { readFileSync as
|
|
6405
|
+
import { readFileSync as readFileSync22 } from "fs";
|
|
6140
6406
|
import { homedir } from "os";
|
|
6141
|
-
import { join as
|
|
6407
|
+
import { join as join16 } from "path";
|
|
6142
6408
|
function readJson2(path) {
|
|
6143
6409
|
try {
|
|
6144
|
-
const parsed = JSON.parse(
|
|
6410
|
+
const parsed = JSON.parse(readFileSync22(path, "utf8"));
|
|
6145
6411
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
6146
6412
|
} catch {
|
|
6147
6413
|
return {};
|
|
@@ -6166,7 +6432,7 @@ var init_ui_prefs = __esm({
|
|
|
6166
6432
|
THEMES = ["system", "light", "dark"];
|
|
6167
6433
|
isThemeMode = (v) => THEMES.includes(v);
|
|
6168
6434
|
isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
|
|
6169
|
-
defaultUiPrefsPath = () =>
|
|
6435
|
+
defaultUiPrefsPath = () => join16(homedir(), ".glotfile", "ui.json");
|
|
6170
6436
|
DEFAULTS = { theme: "system" };
|
|
6171
6437
|
}
|
|
6172
6438
|
});
|
|
@@ -6174,19 +6440,34 @@ var init_ui_prefs = __esm({
|
|
|
6174
6440
|
// src/server/api.ts
|
|
6175
6441
|
import { Hono } from "hono";
|
|
6176
6442
|
import { streamSSE } from "hono/streaming";
|
|
6177
|
-
import { readFileSync as
|
|
6443
|
+
import { readFileSync as readFileSync23, existsSync as existsSync13, readdirSync as readdirSync14, statSync as statSync8, rmSync as rmSync6 } from "fs";
|
|
6178
6444
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
6179
6445
|
function projectName(root) {
|
|
6180
6446
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
6181
|
-
if (
|
|
6447
|
+
if (existsSync13(nameFile)) {
|
|
6182
6448
|
try {
|
|
6183
|
-
const name =
|
|
6449
|
+
const name = readFileSync23(nameFile, "utf8").trim();
|
|
6184
6450
|
if (name) return name;
|
|
6185
6451
|
} catch {
|
|
6186
6452
|
}
|
|
6187
6453
|
}
|
|
6188
6454
|
return basename(root);
|
|
6189
6455
|
}
|
|
6456
|
+
function attachUsageSnippets(targets, cache2, projectRoot) {
|
|
6457
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
6458
|
+
for (const target of targets) {
|
|
6459
|
+
const allRefs = Object.entries(cache2.files).flatMap(
|
|
6460
|
+
([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
6461
|
+
key: r.key,
|
|
6462
|
+
file,
|
|
6463
|
+
line: r.line,
|
|
6464
|
+
col: r.col,
|
|
6465
|
+
scanner: r.scanner
|
|
6466
|
+
}))
|
|
6467
|
+
);
|
|
6468
|
+
target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
|
|
6469
|
+
}
|
|
6470
|
+
}
|
|
6190
6471
|
function createApi(deps) {
|
|
6191
6472
|
const app = new Hono();
|
|
6192
6473
|
const load = () => loadState(deps.statePath);
|
|
@@ -6313,7 +6594,7 @@ function createApi(deps) {
|
|
|
6313
6594
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
6314
6595
|
const abs = resolve9(dir, name);
|
|
6315
6596
|
let filePath = null;
|
|
6316
|
-
if ((name === "glotfile" || name.endsWith(".glotfile")) &&
|
|
6597
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync13(resolve9(abs, "config.json"))) {
|
|
6317
6598
|
filePath = resolve9(dir, `${name}.json`);
|
|
6318
6599
|
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
6319
6600
|
filePath = abs;
|
|
@@ -6347,7 +6628,7 @@ function createApi(deps) {
|
|
|
6347
6628
|
const resolved = resolve9(projectRoot, path);
|
|
6348
6629
|
const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
|
|
6349
6630
|
if (!inside) return c.json({ error: "file is outside the project" }, 400);
|
|
6350
|
-
if (!
|
|
6631
|
+
if (!existsSync13(resolved)) return c.json({ error: "file not found" }, 400);
|
|
6351
6632
|
loadState(resolved);
|
|
6352
6633
|
deps.statePath = resolved;
|
|
6353
6634
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
@@ -6408,9 +6689,9 @@ function createApi(deps) {
|
|
|
6408
6689
|
const abs = resolve9(root, screenshot);
|
|
6409
6690
|
const rel = relative4(root, abs);
|
|
6410
6691
|
const seg0 = rel.split(sep2)[0] ?? "";
|
|
6411
|
-
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") &&
|
|
6692
|
+
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync13(abs)) {
|
|
6412
6693
|
try {
|
|
6413
|
-
|
|
6694
|
+
rmSync6(abs);
|
|
6414
6695
|
} catch {
|
|
6415
6696
|
}
|
|
6416
6697
|
}
|
|
@@ -6836,6 +7117,7 @@ function createApi(deps) {
|
|
|
6836
7117
|
persist(fresh);
|
|
6837
7118
|
totalWritten += written;
|
|
6838
7119
|
allErrors.push(...errors);
|
|
7120
|
+
const usage = provider.takeUsage?.();
|
|
6839
7121
|
appendLog(projectRoot, {
|
|
6840
7122
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6841
7123
|
kind: "translate",
|
|
@@ -6846,7 +7128,9 @@ function createApi(deps) {
|
|
|
6846
7128
|
const req = reqById.get(r.id);
|
|
6847
7129
|
return { id: r.id, key: req?.key ?? "", source: req?.source ?? "", targetLocale: req?.targetLocale, context: req?.context, glossary: req?.glossary, screenshot: req ? fresh.keys[req.key]?.screenshot : void 0 };
|
|
6848
7130
|
}),
|
|
6849
|
-
results: batchResults
|
|
7131
|
+
results: batchResults,
|
|
7132
|
+
usage,
|
|
7133
|
+
estimatedCostUsd: usageCostUsd(usage, aiCfg)
|
|
6850
7134
|
});
|
|
6851
7135
|
const ld = (localeDone.get(locale) ?? 0) + batchResults.length;
|
|
6852
7136
|
localeDone.set(locale, ld);
|
|
@@ -6918,11 +7202,14 @@ function createApi(deps) {
|
|
|
6918
7202
|
}, aiCfg.concurrency, void 0, aiCfg.batchSize);
|
|
6919
7203
|
const latest = load();
|
|
6920
7204
|
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
7205
|
+
const usage = provider.takeUsage?.();
|
|
6921
7206
|
const entry = {
|
|
6922
7207
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6923
7208
|
kind: "translate",
|
|
6924
7209
|
summary: `Translated ${toTranslate.length} item(s)`,
|
|
6925
7210
|
model: aiCfg.model,
|
|
7211
|
+
usage,
|
|
7212
|
+
estimatedCostUsd: usageCostUsd(usage, aiCfg),
|
|
6926
7213
|
system: buildSystemPrompt(toTranslate.some((r) => r.plural !== void 0)),
|
|
6927
7214
|
// Log the screenshot PATH only — never the image bytes.
|
|
6928
7215
|
items: toTranslate.map((r) => ({
|
|
@@ -7020,17 +7307,7 @@ function createApi(deps) {
|
|
|
7020
7307
|
if (!supportsBatchTranslate(provider)) {
|
|
7021
7308
|
return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
|
|
7022
7309
|
}
|
|
7023
|
-
const outcome = await applyBatchResults(load, persist, provider, pending, projectRoot,
|
|
7024
|
-
batchSize: aiCfg.batchSize,
|
|
7025
|
-
concurrency: aiCfg.concurrency
|
|
7026
|
-
});
|
|
7027
|
-
appendLog(projectRoot, {
|
|
7028
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7029
|
-
kind: "translate",
|
|
7030
|
-
summary: `Applied batch ${pending.batchId}: wrote ${outcome.written}, ${outcome.retried} retried, ${outcome.staleSkipped} stale`,
|
|
7031
|
-
model: aiCfg.model,
|
|
7032
|
-
results: []
|
|
7033
|
-
});
|
|
7310
|
+
const outcome = await applyBatchResults(load, persist, provider, pending, projectRoot, aiCfg);
|
|
7034
7311
|
console.log(`[batch] applied ${pending.batchId} \u2014 wrote ${outcome.written}, ${outcome.errors.length} error(s)`);
|
|
7035
7312
|
return c.json(outcome);
|
|
7036
7313
|
}));
|
|
@@ -7142,19 +7419,7 @@ function createApi(deps) {
|
|
|
7142
7419
|
return;
|
|
7143
7420
|
}
|
|
7144
7421
|
await stream.writeSSE({ event: "start", data: JSON.stringify({ total: targets.length }) });
|
|
7145
|
-
|
|
7146
|
-
for (const target of targets) {
|
|
7147
|
-
const allRefs = Object.entries(cache2.files).flatMap(
|
|
7148
|
-
([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
7149
|
-
key: r.key,
|
|
7150
|
-
file,
|
|
7151
|
-
line: r.line,
|
|
7152
|
-
col: r.col,
|
|
7153
|
-
scanner: r.scanner
|
|
7154
|
-
}))
|
|
7155
|
-
);
|
|
7156
|
-
target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
|
|
7157
|
-
}
|
|
7422
|
+
attachUsageSnippets(targets, cache2, projectRoot);
|
|
7158
7423
|
const system = buildContextSystemPrompt();
|
|
7159
7424
|
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
7160
7425
|
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
@@ -7181,6 +7446,7 @@ function createApi(deps) {
|
|
|
7181
7446
|
const batch = raw;
|
|
7182
7447
|
const fresh = load();
|
|
7183
7448
|
const { written, errors } = applyContext(fresh, chunk2, batch.items ?? [], void 0, body.force === true);
|
|
7449
|
+
const usage = provider.takeUsage?.();
|
|
7184
7450
|
appendLog(projectRoot, {
|
|
7185
7451
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7186
7452
|
kind: "context",
|
|
@@ -7188,7 +7454,9 @@ function createApi(deps) {
|
|
|
7188
7454
|
model: aiCfg.model,
|
|
7189
7455
|
system,
|
|
7190
7456
|
items: chunk2.map((t) => ({ id: t.id, key: t.key, source: t.source })),
|
|
7191
|
-
results: (batch.items ?? []).map((r) => ({ id: r.id, value: r.context, error: r.error }))
|
|
7457
|
+
results: (batch.items ?? []).map((r) => ({ id: r.id, value: r.context, error: r.error })),
|
|
7458
|
+
usage,
|
|
7459
|
+
estimatedCostUsd: usageCostUsd(usage, aiCfg)
|
|
7192
7460
|
});
|
|
7193
7461
|
persist(fresh);
|
|
7194
7462
|
totalWritten += written;
|
|
@@ -7203,6 +7471,100 @@ function createApi(deps) {
|
|
|
7203
7471
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: targets.length, written: totalWritten, errors: allErrors }) });
|
|
7204
7472
|
});
|
|
7205
7473
|
});
|
|
7474
|
+
app.get("/context/batch/status", async (c) => {
|
|
7475
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7476
|
+
let supported = false;
|
|
7477
|
+
let provider;
|
|
7478
|
+
try {
|
|
7479
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7480
|
+
supported = supportsBatchComplete(provider);
|
|
7481
|
+
} catch {
|
|
7482
|
+
}
|
|
7483
|
+
const pending = loadPendingContextBatch(projectRoot);
|
|
7484
|
+
if (!pending) return c.json({ supported, pending: null });
|
|
7485
|
+
const base = { batchId: pending.batchId, createdAt: pending.createdAt, model: pending.model, total: pending.total };
|
|
7486
|
+
if (!provider || !supportsBatchComplete(provider)) {
|
|
7487
|
+
return c.json({ supported, pending: { ...base, status: "unknown", counts: null } });
|
|
7488
|
+
}
|
|
7489
|
+
try {
|
|
7490
|
+
const status = await provider.translationBatchStatus(pending.batchId);
|
|
7491
|
+
return c.json({ supported, pending: { ...base, status: status.status, counts: status.counts } });
|
|
7492
|
+
} catch (e) {
|
|
7493
|
+
return c.json({ supported, pending: { ...base, status: "unknown", counts: null, error: e.message } });
|
|
7494
|
+
}
|
|
7495
|
+
});
|
|
7496
|
+
app.post("/context/batch", (c) => withTranslateLock(async () => {
|
|
7497
|
+
const body = await c.req.json().catch(() => ({}));
|
|
7498
|
+
const s = load();
|
|
7499
|
+
const cache2 = loadUsageCache(projectRoot);
|
|
7500
|
+
if (!cache2) return c.json({ error: "No usage index found. Run 'glotfile scan' first." }, 400);
|
|
7501
|
+
const targets = selectContextTargets(s, {
|
|
7502
|
+
all: body.all,
|
|
7503
|
+
keyGlob: body.keyGlob,
|
|
7504
|
+
limit: body.limit,
|
|
7505
|
+
since: body.since,
|
|
7506
|
+
keys: body.keys,
|
|
7507
|
+
force: body.force
|
|
7508
|
+
}, cache2, body.lastRunAt);
|
|
7509
|
+
if (!targets.length) return c.json({ error: "Nothing to build." }, 400);
|
|
7510
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7511
|
+
let provider;
|
|
7512
|
+
try {
|
|
7513
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7514
|
+
} catch (e) {
|
|
7515
|
+
return c.json({ error: e.message }, 400);
|
|
7516
|
+
}
|
|
7517
|
+
if (!supportsBatchComplete(provider)) {
|
|
7518
|
+
return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
|
|
7519
|
+
}
|
|
7520
|
+
attachUsageSnippets(targets, cache2, projectRoot);
|
|
7521
|
+
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
7522
|
+
let pending;
|
|
7523
|
+
try {
|
|
7524
|
+
pending = await submitContextBatch(provider, targets, batchSize, aiCfg.model, projectRoot, body.force === true);
|
|
7525
|
+
} catch (e) {
|
|
7526
|
+
return c.json({ error: e.message }, 409);
|
|
7527
|
+
}
|
|
7528
|
+
appendLog(projectRoot, {
|
|
7529
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7530
|
+
kind: "context",
|
|
7531
|
+
summary: `Submitted context batch ${pending.batchId} (${pending.total} keys)`,
|
|
7532
|
+
model: aiCfg.model,
|
|
7533
|
+
system: buildContextSystemPrompt(),
|
|
7534
|
+
items: targets.map((t) => ({ id: t.id, key: t.key, source: t.source }))
|
|
7535
|
+
});
|
|
7536
|
+
console.log(`[context-batch] submitted ${pending.batchId} \u2014 ${pending.total} key(s)`);
|
|
7537
|
+
return c.json({ batchId: pending.batchId, total: pending.total });
|
|
7538
|
+
}));
|
|
7539
|
+
app.post("/context/batch/apply", (c) => withTranslateLock(async () => {
|
|
7540
|
+
const pending = loadPendingContextBatch(projectRoot);
|
|
7541
|
+
if (!pending) return c.json({ error: "No pending context batch." }, 404);
|
|
7542
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7543
|
+
let provider;
|
|
7544
|
+
try {
|
|
7545
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7546
|
+
} catch (e) {
|
|
7547
|
+
return c.json({ error: e.message }, 400);
|
|
7548
|
+
}
|
|
7549
|
+
if (!supportsBatchComplete(provider)) {
|
|
7550
|
+
return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
|
|
7551
|
+
}
|
|
7552
|
+
const outcome = await applyContextBatchResults(load, persist, provider, pending, projectRoot, aiCfg);
|
|
7553
|
+
console.log(`[context-batch] applied ${pending.batchId} \u2014 wrote ${outcome.written}, ${outcome.errors.length} error(s)`);
|
|
7554
|
+
return c.json(outcome);
|
|
7555
|
+
}));
|
|
7556
|
+
app.post("/context/batch/cancel", async (c) => {
|
|
7557
|
+
const pending = loadPendingContextBatch(projectRoot);
|
|
7558
|
+
if (!pending) return c.json({ error: "No pending context batch." }, 404);
|
|
7559
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7560
|
+
try {
|
|
7561
|
+
const provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7562
|
+
if (supportsBatchComplete(provider)) await provider.cancelTranslationBatch(pending.batchId);
|
|
7563
|
+
} catch {
|
|
7564
|
+
}
|
|
7565
|
+
clearPendingContextBatch(projectRoot);
|
|
7566
|
+
return c.json({ canceled: pending.batchId });
|
|
7567
|
+
});
|
|
7206
7568
|
app.onError(
|
|
7207
7569
|
(err, c) => c.json({ error: err.message }, err instanceof GlotfileError ? 400 : 500)
|
|
7208
7570
|
);
|
|
@@ -7228,7 +7590,10 @@ var init_api = __esm({
|
|
|
7228
7590
|
init_provider();
|
|
7229
7591
|
init_batch_run();
|
|
7230
7592
|
init_pending_batch();
|
|
7593
|
+
init_context_batch_run();
|
|
7594
|
+
init_pending_context_batch();
|
|
7231
7595
|
init_estimate();
|
|
7596
|
+
init_pricing();
|
|
7232
7597
|
init_log();
|
|
7233
7598
|
init_schema();
|
|
7234
7599
|
init_run3();
|
|
@@ -7250,7 +7615,7 @@ __export(server_exports, {
|
|
|
7250
7615
|
import { Hono as Hono2 } from "hono";
|
|
7251
7616
|
import { serve } from "@hono/node-server";
|
|
7252
7617
|
import { fileURLToPath } from "url";
|
|
7253
|
-
import { dirname as dirname4, join as
|
|
7618
|
+
import { dirname as dirname4, join as join17, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
7254
7619
|
import { readFile, stat } from "fs/promises";
|
|
7255
7620
|
import { createServer } from "net";
|
|
7256
7621
|
import open from "open";
|
|
@@ -7293,7 +7658,7 @@ function buildApp(opts) {
|
|
|
7293
7658
|
const file = await readFileResponse(target);
|
|
7294
7659
|
if (file) return file;
|
|
7295
7660
|
}
|
|
7296
|
-
const index = await readFileResponse(
|
|
7661
|
+
const index = await readFileResponse(join17(root, "index.html"));
|
|
7297
7662
|
if (index) return index;
|
|
7298
7663
|
return c.notFound();
|
|
7299
7664
|
});
|
|
@@ -7351,7 +7716,7 @@ var init_server = __esm({
|
|
|
7351
7716
|
init_scan();
|
|
7352
7717
|
init_scanner();
|
|
7353
7718
|
here = dirname4(fileURLToPath(import.meta.url));
|
|
7354
|
-
DEFAULT_UI_DIR =
|
|
7719
|
+
DEFAULT_UI_DIR = join17(here, "..", "ui");
|
|
7355
7720
|
MIME = {
|
|
7356
7721
|
".html": "text/html; charset=utf-8",
|
|
7357
7722
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -7390,15 +7755,18 @@ init_run();
|
|
|
7390
7755
|
init_provider();
|
|
7391
7756
|
init_batch_run();
|
|
7392
7757
|
init_pending_batch();
|
|
7758
|
+
init_context_batch_run();
|
|
7759
|
+
init_pending_context_batch();
|
|
7393
7760
|
init_estimate();
|
|
7761
|
+
init_pricing();
|
|
7394
7762
|
init_log();
|
|
7395
7763
|
init_scan();
|
|
7396
7764
|
init_scanner();
|
|
7397
7765
|
init_context();
|
|
7398
7766
|
init_run2();
|
|
7399
7767
|
init_outputs();
|
|
7400
|
-
import { resolve as resolve11, dirname as dirname5, join as
|
|
7401
|
-
import { readFileSync as
|
|
7768
|
+
import { resolve as resolve11, dirname as dirname5, join as join18 } from "path";
|
|
7769
|
+
import { readFileSync as readFileSync24, existsSync as existsSync14, mkdirSync as mkdirSync6, cpSync } from "fs";
|
|
7402
7770
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7403
7771
|
|
|
7404
7772
|
// src/server/lint/locate.ts
|
|
@@ -7713,11 +8081,14 @@ async function runTranslate(args) {
|
|
|
7713
8081
|
if (!batchCallbackFired) {
|
|
7714
8082
|
({ written, errors } = applyResults(state, toTranslate, results));
|
|
7715
8083
|
}
|
|
8084
|
+
const usage = provider.takeUsage?.();
|
|
7716
8085
|
appendLog(projectRoot, {
|
|
7717
8086
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7718
8087
|
kind: "translate",
|
|
7719
8088
|
summary: `Translated ${toTranslate.length} item(s)`,
|
|
7720
8089
|
model: ai.model,
|
|
8090
|
+
usage,
|
|
8091
|
+
estimatedCostUsd: usageCostUsd(usage, ai),
|
|
7721
8092
|
system: buildSystemPrompt(toTranslate.some((r) => r.plural !== void 0)),
|
|
7722
8093
|
items: toTranslate.map((r) => ({
|
|
7723
8094
|
id: r.id,
|
|
@@ -7752,7 +8123,7 @@ async function applyPending(args, provider, pending, ai) {
|
|
|
7752
8123
|
provider,
|
|
7753
8124
|
pending,
|
|
7754
8125
|
projectRoot,
|
|
7755
|
-
|
|
8126
|
+
ai
|
|
7756
8127
|
);
|
|
7757
8128
|
reportApply(outcome);
|
|
7758
8129
|
}
|
|
@@ -7770,11 +8141,16 @@ async function waitAndApply(args, provider, pending, ai) {
|
|
|
7770
8141
|
async function runBatch(args) {
|
|
7771
8142
|
const projectRoot = dirname5(resolve11(args.statePath));
|
|
7772
8143
|
const pending = loadPendingBatch(projectRoot);
|
|
7773
|
-
|
|
7774
|
-
|
|
8144
|
+
const ctxPending = loadPendingContextBatch(projectRoot);
|
|
8145
|
+
if (!pending && !ctxPending) {
|
|
8146
|
+
console.log("No pending batch. Start one with `glotfile translate --batch` or `glotfile build-context --batch`.");
|
|
7775
8147
|
return;
|
|
7776
8148
|
}
|
|
7777
8149
|
const action = args.batchAction ?? "status";
|
|
8150
|
+
if (pending) await runTranslationBatchAction(args, pending, action, projectRoot);
|
|
8151
|
+
if (ctxPending) await runContextBatchAction(args, ctxPending, action, projectRoot);
|
|
8152
|
+
}
|
|
8153
|
+
async function runTranslationBatchAction(args, pending, action, projectRoot) {
|
|
7778
8154
|
if (action === "cancel") {
|
|
7779
8155
|
let remoteFailed = false;
|
|
7780
8156
|
try {
|
|
@@ -7811,6 +8187,53 @@ async function runBatch(args) {
|
|
|
7811
8187
|
}
|
|
7812
8188
|
await applyPending(args, provider, pending, ai);
|
|
7813
8189
|
}
|
|
8190
|
+
async function runContextBatchAction(args, pending, action, projectRoot) {
|
|
8191
|
+
if (action === "cancel") {
|
|
8192
|
+
let remoteFailed = false;
|
|
8193
|
+
try {
|
|
8194
|
+
const ai2 = loadLocalSettings(projectRoot).ai;
|
|
8195
|
+
const provider2 = makeProvider(ai2);
|
|
8196
|
+
if (supportsBatchComplete(provider2)) {
|
|
8197
|
+
await provider2.cancelTranslationBatch(pending.batchId);
|
|
8198
|
+
} else {
|
|
8199
|
+
remoteFailed = true;
|
|
8200
|
+
}
|
|
8201
|
+
} catch {
|
|
8202
|
+
remoteFailed = true;
|
|
8203
|
+
}
|
|
8204
|
+
clearPendingContextBatch(projectRoot);
|
|
8205
|
+
const suffix = remoteFailed ? " (remote cancel failed \u2014 it will expire server-side)" : "";
|
|
8206
|
+
console.log(`Canceled context batch ${pending.batchId}.${suffix}`);
|
|
8207
|
+
return;
|
|
8208
|
+
}
|
|
8209
|
+
const ai = loadLocalSettings(projectRoot).ai;
|
|
8210
|
+
const provider = makeProviderOrExit(ai);
|
|
8211
|
+
if (!provider) return;
|
|
8212
|
+
if (!supportsBatchComplete(provider)) {
|
|
8213
|
+
console.error(`Pending context batch was submitted via anthropic, but the configured provider "${ai.provider}" has no batch support.`);
|
|
8214
|
+
process.exitCode = 1;
|
|
8215
|
+
return;
|
|
8216
|
+
}
|
|
8217
|
+
const status = await provider.translationBatchStatus(pending.batchId);
|
|
8218
|
+
const c = status.counts;
|
|
8219
|
+
console.log(`Context batch ${pending.batchId} (${pending.total} key(s), submitted ${pending.createdAt})`);
|
|
8220
|
+
console.log(` ${status.status} \u2014 ${c.succeeded} succeeded, ${c.processing} processing, ${c.errored} errored, ${c.expired} expired, ${c.canceled} canceled`);
|
|
8221
|
+
if (status.status !== "ended") {
|
|
8222
|
+
if (action === "apply") console.log("Not finished yet \u2014 try again later.");
|
|
8223
|
+
return;
|
|
8224
|
+
}
|
|
8225
|
+
const outcome = await applyContextBatchResults(
|
|
8226
|
+
() => loadState(args.statePath),
|
|
8227
|
+
(s) => saveState(args.statePath, s),
|
|
8228
|
+
provider,
|
|
8229
|
+
pending,
|
|
8230
|
+
projectRoot,
|
|
8231
|
+
ai
|
|
8232
|
+
);
|
|
8233
|
+
console.log(`Wrote context for ${outcome.written} key(s).`);
|
|
8234
|
+
if (outcome.retried) console.log(`${outcome.retried} job(s) re-run synchronously (batch entries failed or were malformed).`);
|
|
8235
|
+
for (const e of outcome.errors) console.warn(`skip ${e.key}: ${e.error}`);
|
|
8236
|
+
}
|
|
7814
8237
|
function printReport(report, format, rawText) {
|
|
7815
8238
|
if (format === "json") console.log(formatJson(report).trimEnd());
|
|
7816
8239
|
else if (format === "sarif") console.log(formatSarif(report, rawText).trimEnd());
|
|
@@ -7830,7 +8253,7 @@ async function runLintCmd(args) {
|
|
|
7830
8253
|
}
|
|
7831
8254
|
return;
|
|
7832
8255
|
}
|
|
7833
|
-
const rawText =
|
|
8256
|
+
const rawText = existsSync14(args.statePath) ? readFileSync24(args.statePath, "utf8") : "";
|
|
7834
8257
|
const report = await runLint(state, {
|
|
7835
8258
|
locales: args.locales,
|
|
7836
8259
|
ruleIds: args.ruleIds,
|
|
@@ -7854,7 +8277,7 @@ async function runCheck(args) {
|
|
|
7854
8277
|
process.exitCode = 1;
|
|
7855
8278
|
return;
|
|
7856
8279
|
}
|
|
7857
|
-
const rawText =
|
|
8280
|
+
const rawText = existsSync14(args.statePath) ? readFileSync24(args.statePath, "utf8") : "";
|
|
7858
8281
|
const root = dirname5(resolve11(args.statePath));
|
|
7859
8282
|
const lint = await runLint(state, {});
|
|
7860
8283
|
const findings = sortFindings([...lint.findings, ...checkOutputs(state, root)]);
|
|
@@ -7867,7 +8290,7 @@ async function runImportCmd(args) {
|
|
|
7867
8290
|
const { runImport: runImport2 } = await Promise.resolve().then(() => (init_run3(), run_exports));
|
|
7868
8291
|
const projectRoot = args.importSource ? resolve11(args.importSource) : dirname5(resolve11(args.statePath));
|
|
7869
8292
|
const out = resolve11(projectRoot, "glotfile.json");
|
|
7870
|
-
if (
|
|
8293
|
+
if (existsSync14(out) && !args.importForce) {
|
|
7871
8294
|
console.error(`${out} already exists; pass --force to overwrite`);
|
|
7872
8295
|
process.exitCode = 1;
|
|
7873
8296
|
return;
|
|
@@ -7934,6 +8357,32 @@ async function runBuildContext(args) {
|
|
|
7934
8357
|
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7935
8358
|
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
7936
8359
|
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
8360
|
+
if (args.batch) {
|
|
8361
|
+
if (!supportsBatchComplete(provider)) {
|
|
8362
|
+
console.error(`Provider "${aiCfg.provider}" does not support batch mode. Currently anthropic only.`);
|
|
8363
|
+
process.exitCode = 1;
|
|
8364
|
+
return;
|
|
8365
|
+
}
|
|
8366
|
+
let pending;
|
|
8367
|
+
try {
|
|
8368
|
+
pending = await submitContextBatch(provider, targets, batchSize, aiCfg.model, projectRoot, false);
|
|
8369
|
+
} catch (e) {
|
|
8370
|
+
console.error(e.message);
|
|
8371
|
+
process.exitCode = 1;
|
|
8372
|
+
return;
|
|
8373
|
+
}
|
|
8374
|
+
appendLog(projectRoot, {
|
|
8375
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8376
|
+
kind: "context",
|
|
8377
|
+
summary: `Submitted context batch ${pending.batchId} (${pending.total} keys)`,
|
|
8378
|
+
model: aiCfg.model,
|
|
8379
|
+
system,
|
|
8380
|
+
items: targets.map((t) => ({ id: t.id, key: t.key, source: t.source }))
|
|
8381
|
+
});
|
|
8382
|
+
console.log(`Submitted context batch ${pending.batchId} \u2014 ${pending.total} key(s) at 50% batch pricing.`);
|
|
8383
|
+
console.log("Check progress with `glotfile batch`; it applies results automatically when finished.");
|
|
8384
|
+
return;
|
|
8385
|
+
}
|
|
7937
8386
|
const chunks = [];
|
|
7938
8387
|
for (let i = 0; i < targets.length; i += batchSize) chunks.push(targets.slice(i, i + batchSize));
|
|
7939
8388
|
let written = 0;
|
|
@@ -8015,19 +8464,19 @@ function runSplit(args) {
|
|
|
8015
8464
|
`Split catalog into ${splitDirFor(args.statePath)}/ (config.json, keys.json, locales/ \u2014 up to ${state.config.locales.length} locale files). Removed ${args.statePath}.`
|
|
8016
8465
|
);
|
|
8017
8466
|
}
|
|
8018
|
-
var SKILL_SRC =
|
|
8467
|
+
var SKILL_SRC = join18(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "skill");
|
|
8019
8468
|
function runSkill(args) {
|
|
8020
8469
|
if (args.print) {
|
|
8021
|
-
console.log(
|
|
8470
|
+
console.log(readFileSync24(join18(SKILL_SRC, "SKILL.md"), "utf8").trimEnd());
|
|
8022
8471
|
return;
|
|
8023
8472
|
}
|
|
8024
8473
|
const dest = resolve11(process.cwd(), ".claude", "skills", "glotfile");
|
|
8025
|
-
if (
|
|
8474
|
+
if (existsSync14(dest) && !args.importForce) {
|
|
8026
8475
|
console.error(`${dest} already exists; pass --force to overwrite`);
|
|
8027
8476
|
process.exitCode = 1;
|
|
8028
8477
|
return;
|
|
8029
8478
|
}
|
|
8030
|
-
|
|
8479
|
+
mkdirSync6(dirname5(dest), { recursive: true });
|
|
8031
8480
|
cpSync(SKILL_SRC, dest, { recursive: true });
|
|
8032
8481
|
console.log(`Installed the glotfile skill to ${dest}. Restart Claude Code to pick it up.`);
|
|
8033
8482
|
}
|
|
@@ -8092,7 +8541,7 @@ var COMMAND_HELP = {
|
|
|
8092
8541
|
},
|
|
8093
8542
|
"build-context": {
|
|
8094
8543
|
summary: "AI-generate per-key context to improve translation (requires a prior scan).",
|
|
8095
|
-
usage: "glotfile build-context [--all] [--key <glob>] [--limit <n>] [--since <date>]",
|
|
8544
|
+
usage: "glotfile build-context [--all] [--key <glob>] [--limit <n>] [--since <date>] [--batch]",
|
|
8096
8545
|
options: [
|
|
8097
8546
|
["--all", "(Re)build context for every key, not just those missing it"],
|
|
8098
8547
|
["--key <glob>", "Only keys matching this glob"],
|
|
@@ -8167,8 +8616,8 @@ ${formatOpts([...options, ...GLOBAL_OPTS])}`);
|
|
|
8167
8616
|
);
|
|
8168
8617
|
}
|
|
8169
8618
|
function printVersion() {
|
|
8170
|
-
const pkgPath =
|
|
8171
|
-
console.log(JSON.parse(
|
|
8619
|
+
const pkgPath = join18(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
|
|
8620
|
+
console.log(JSON.parse(readFileSync24(pkgPath, "utf8")).version);
|
|
8172
8621
|
}
|
|
8173
8622
|
async function main(argv) {
|
|
8174
8623
|
const args = parseArgs(argv);
|