glotfile 0.7.2 → 0.7.5
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 +825 -465
- package/dist/server/server.js +361 -97
- package/dist/ui/assets/{index-D04CSFY5.js → index-BvrhsGHu.js} +5 -5
- package/dist/ui/assets/index-dSBo_QMR.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-DNjcY2ek.css +0 -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.",
|
|
@@ -2346,10 +2349,13 @@ var init_anthropic = __esm({
|
|
|
2346
2349
|
content.push({ type: "text", text: buildBatchPrompt(batch) });
|
|
2347
2350
|
return content;
|
|
2348
2351
|
}
|
|
2349
|
-
|
|
2350
|
-
|
|
2352
|
+
completionContent(req) {
|
|
2353
|
+
return req.content.map(
|
|
2351
2354
|
(b) => b.type === "image" ? { type: "image", source: { type: "base64", media_type: b.mediaType, data: b.base64 } } : { type: "text", text: b.text ?? "" }
|
|
2352
2355
|
);
|
|
2356
|
+
}
|
|
2357
|
+
async complete(req) {
|
|
2358
|
+
const content = this.completionContent(req);
|
|
2353
2359
|
const res = await this.client.messages.create({
|
|
2354
2360
|
model: this.config.model,
|
|
2355
2361
|
max_tokens: req.maxTokens ?? 8192,
|
|
@@ -2413,6 +2419,40 @@ var init_anthropic = __esm({
|
|
|
2413
2419
|
async cancelTranslationBatch(batchId) {
|
|
2414
2420
|
await this.batchesClient().cancel(batchId);
|
|
2415
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
|
+
}
|
|
2416
2456
|
async callBatch(batch, signal) {
|
|
2417
2457
|
const content = this.buildUserContent(batch);
|
|
2418
2458
|
const res = await this.client.messages.create({
|
|
@@ -3395,101 +3435,403 @@ var init_batch_run = __esm({
|
|
|
3395
3435
|
}
|
|
3396
3436
|
});
|
|
3397
3437
|
|
|
3398
|
-
// src/server/ai/
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
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}$`);
|
|
3402
3444
|
}
|
|
3403
|
-
function
|
|
3404
|
-
const
|
|
3405
|
-
|
|
3406
|
-
|
|
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
|
+
});
|
|
3407
3468
|
}
|
|
3408
|
-
return
|
|
3469
|
+
return snippets;
|
|
3409
3470
|
}
|
|
3410
|
-
function
|
|
3411
|
-
const
|
|
3412
|
-
const
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
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);
|
|
3418
3478
|
}
|
|
3419
|
-
group.push(r);
|
|
3420
3479
|
}
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
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;
|
|
3430
3496
|
}
|
|
3431
|
-
|
|
3497
|
+
const source = entry.values[state.config.sourceLocale]?.value ?? "";
|
|
3498
|
+
candidates.push({ id: String(candidates.length), key, source, usageSnippets: [] });
|
|
3432
3499
|
}
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
3444
|
-
};
|
|
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;
|
|
3445
3510
|
}
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
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"() {
|
|
3449
3570
|
"use strict";
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
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
|
+
};
|
|
3458
3596
|
}
|
|
3459
3597
|
});
|
|
3460
3598
|
|
|
3461
|
-
// src/server/
|
|
3462
|
-
import { existsSync as
|
|
3463
|
-
import {
|
|
3464
|
-
function
|
|
3465
|
-
|
|
3466
|
-
|
|
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;
|
|
3467
3608
|
try {
|
|
3468
|
-
|
|
3609
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
3610
|
+
if (parsed?.version !== 1) return void 0;
|
|
3611
|
+
return parsed;
|
|
3469
3612
|
} catch {
|
|
3470
|
-
return
|
|
3613
|
+
return void 0;
|
|
3471
3614
|
}
|
|
3472
3615
|
}
|
|
3473
|
-
function
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
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");
|
|
3477
3622
|
}
|
|
3478
|
-
function
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
for (const locale of targets) {
|
|
3485
|
-
const v = entry.plural ? entry.values[locale]?.forms?.other?.trim() : entry.values[locale]?.value?.trim();
|
|
3486
|
-
if (!v) out.push({ key, locale });
|
|
3487
|
-
}
|
|
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";
|
|
3488
3629
|
}
|
|
3489
|
-
|
|
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
|
+
};
|
|
3490
3639
|
}
|
|
3491
|
-
function
|
|
3492
|
-
|
|
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 {
|
|
3779
|
+
requests: reqs.length,
|
|
3780
|
+
batches: perLocale.reduce((n, l) => n + l.batches, 0),
|
|
3781
|
+
perLocale,
|
|
3782
|
+
inputTokens,
|
|
3783
|
+
outputTokens,
|
|
3784
|
+
pricing,
|
|
3785
|
+
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
3786
|
+
};
|
|
3787
|
+
}
|
|
3788
|
+
var CJK_RE, EXPANSION, ITEM_REPLY_OVERHEAD, FORM_REPLY_OVERHEAD;
|
|
3789
|
+
var init_estimate = __esm({
|
|
3790
|
+
"src/server/ai/estimate.ts"() {
|
|
3791
|
+
"use strict";
|
|
3792
|
+
init_run();
|
|
3793
|
+
init_provider();
|
|
3794
|
+
init_batch();
|
|
3795
|
+
init_pricing();
|
|
3796
|
+
CJK_RE = /[ -鿿가-豈-]/g;
|
|
3797
|
+
EXPANSION = 1.2;
|
|
3798
|
+
ITEM_REPLY_OVERHEAD = 16;
|
|
3799
|
+
FORM_REPLY_OVERHEAD = 8;
|
|
3800
|
+
}
|
|
3801
|
+
});
|
|
3802
|
+
|
|
3803
|
+
// src/server/scan.ts
|
|
3804
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
3805
|
+
import { resolve as resolve7 } from "path";
|
|
3806
|
+
function loadUsageCache(projectRoot) {
|
|
3807
|
+
const path = resolve7(projectRoot, ".glotfile", "usage.json");
|
|
3808
|
+
if (!existsSync10(path)) return null;
|
|
3809
|
+
try {
|
|
3810
|
+
return JSON.parse(readFileSync10(path, "utf8"));
|
|
3811
|
+
} catch {
|
|
3812
|
+
return null;
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
function saveUsageCache(projectRoot, cache2) {
|
|
3816
|
+
ensureGlotfileDir(projectRoot);
|
|
3817
|
+
const path = resolve7(projectRoot, ".glotfile", "usage.json");
|
|
3818
|
+
writeFileAtomic(path, JSON.stringify(cache2, null, 2) + "\n");
|
|
3819
|
+
}
|
|
3820
|
+
function findMissing(state) {
|
|
3821
|
+
const targets = state.config.locales.filter((l) => l !== state.config.sourceLocale).sort();
|
|
3822
|
+
const out = [];
|
|
3823
|
+
for (const key of Object.keys(state.keys).sort()) {
|
|
3824
|
+
const entry = state.keys[key];
|
|
3825
|
+
if (entry.skipTranslate) continue;
|
|
3826
|
+
for (const locale of targets) {
|
|
3827
|
+
const v = entry.plural ? entry.values[locale]?.forms?.other?.trim() : entry.values[locale]?.value?.trim();
|
|
3828
|
+
if (!v) out.push({ key, locale });
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
return out;
|
|
3832
|
+
}
|
|
3833
|
+
function computeUsedKeys(state, cache2) {
|
|
3834
|
+
const exact = /* @__PURE__ */ new Set();
|
|
3493
3835
|
const prefixes = [];
|
|
3494
3836
|
for (const entry of Object.values(cache2.files)) {
|
|
3495
3837
|
for (const r of entry.refs) exact.add(r.key);
|
|
@@ -3530,8 +3872,8 @@ var init_scan = __esm({
|
|
|
3530
3872
|
});
|
|
3531
3873
|
|
|
3532
3874
|
// src/server/scanner.ts
|
|
3533
|
-
import { readdirSync as readdirSync3, statSync as statSync2, readFileSync as
|
|
3534
|
-
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";
|
|
3535
3877
|
function scannerForExt(ext) {
|
|
3536
3878
|
return EXT_SCANNER[ext] ?? null;
|
|
3537
3879
|
}
|
|
@@ -3683,7 +4025,7 @@ function* walkFiles(dir, root, exclude) {
|
|
|
3683
4025
|
}
|
|
3684
4026
|
for (const name of entries) {
|
|
3685
4027
|
if (ALWAYS_EXCLUDE.has(name)) continue;
|
|
3686
|
-
const abs =
|
|
4028
|
+
const abs = join5(dir, name);
|
|
3687
4029
|
const rel = relative(root, abs);
|
|
3688
4030
|
let st;
|
|
3689
4031
|
try {
|
|
@@ -3713,7 +4055,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
3713
4055
|
const ext = extname2(relPath);
|
|
3714
4056
|
const scanner = scannerForExt(ext);
|
|
3715
4057
|
if (!scanner) continue;
|
|
3716
|
-
const abs =
|
|
4058
|
+
const abs = join5(projectRoot, relPath);
|
|
3717
4059
|
let st;
|
|
3718
4060
|
try {
|
|
3719
4061
|
st = statSync2(abs);
|
|
@@ -3724,284 +4066,123 @@ function runScan(projectRoot, opts, existing) {
|
|
|
3724
4066
|
const size = st.size;
|
|
3725
4067
|
const prev = reusable?.files[relPath];
|
|
3726
4068
|
if (prev && prev.mtime === mtime && prev.size === size) {
|
|
3727
|
-
cache2.files[relPath] = prev;
|
|
3728
|
-
continue;
|
|
3729
|
-
}
|
|
3730
|
-
let content;
|
|
3731
|
-
try {
|
|
3732
|
-
content =
|
|
3733
|
-
} catch {
|
|
3734
|
-
continue;
|
|
3735
|
-
}
|
|
3736
|
-
cache2.files[relPath] = {
|
|
3737
|
-
mtime,
|
|
3738
|
-
size,
|
|
3739
|
-
refs: extractRefs(content, scanner, opts),
|
|
3740
|
-
prefixes: extractPrefixes(content, scanner),
|
|
3741
|
-
literals: extractLiterals(content)
|
|
3742
|
-
};
|
|
3743
|
-
}
|
|
3744
|
-
saveUsageCache(projectRoot, cache2);
|
|
3745
|
-
return cache2;
|
|
3746
|
-
}
|
|
3747
|
-
var PATTERNS, PREFIX_PATTERNS, CACHE_VERSION, EXT_SCANNER, ALWAYS_EXCLUDE, FLUTTER_ACCESSOR_DEFAULTS, KEY_SHAPE, STRING_LITERALS;
|
|
3748
|
-
var init_scanner = __esm({
|
|
3749
|
-
"src/server/scanner.ts"() {
|
|
3750
|
-
"use strict";
|
|
3751
|
-
init_scan();
|
|
3752
|
-
PATTERNS = {
|
|
3753
|
-
laravel: [
|
|
3754
|
-
/\b(?:__|trans|trans_choice|Lang::(?:get|choice))\s*\(\s*'([^']+)'/g,
|
|
3755
|
-
/\b(?:__|trans|trans_choice|Lang::(?:get|choice))\s*\(\s*"([^"]+)"/g,
|
|
3756
|
-
/@(?:lang|choice)\s*\(\s*'([^']+)'/g,
|
|
3757
|
-
/@(?:lang|choice)\s*\(\s*"([^"]+)"/g
|
|
3758
|
-
],
|
|
3759
|
-
"js-i18n": [
|
|
3760
|
-
/\$t\s*\(\s*'([^']+)'/g,
|
|
3761
|
-
/\$t\s*\(\s*"([^"]+)"/g,
|
|
3762
|
-
/\$t\s*\(\s*`([^`$\n]+)`/g,
|
|
3763
|
-
/\bi18n\.t\s*\(\s*'([^']+)'/g,
|
|
3764
|
-
/\bi18n\.t\s*\(\s*"([^"]+)"/g,
|
|
3765
|
-
/\bi18next\.t\s*\(\s*'([^']+)'/g,
|
|
3766
|
-
/\bi18next\.t\s*\(\s*"([^"]+)"/g,
|
|
3767
|
-
// t('key') — word boundary before t, not preceded by dot (excludes i18n.t which is above)
|
|
3768
|
-
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*'([^']+)'/g,
|
|
3769
|
-
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*"([^"]+)"/g,
|
|
3770
|
-
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$\n]+)`/g
|
|
3771
|
-
],
|
|
3772
|
-
gettext: [
|
|
3773
|
-
/\b(?:gettext|ngettext)\s*\(\s*'([^']+)'/g,
|
|
3774
|
-
/\b(?:gettext|ngettext)\s*\(\s*"([^"]+)"/g,
|
|
3775
|
-
// _() — word boundary, not preceded by alphanumeric
|
|
3776
|
-
/(?<![a-zA-Z0-9_$])_\s*\(\s*'([^']+)'/g,
|
|
3777
|
-
/(?<![a-zA-Z0-9_$])_\s*\(\s*"([^"]+)"/g
|
|
3778
|
-
],
|
|
3779
|
-
apple: [
|
|
3780
|
-
/NSLocalizedString\s*\(\s*@?"([^"]+)"/g,
|
|
3781
|
-
/String\s*\(\s*localized:\s*"([^"]+)"/g,
|
|
3782
|
-
/localizedString\s*\(\s*forKey:\s*"([^"]+)"/g,
|
|
3783
|
-
// The "key".localized / "key".localised String-extension idiom, where the
|
|
3784
|
-
// literal IS the key (common when keys are natural-language source text).
|
|
3785
|
-
/"([^"]+)"\s*\.\s*localized\b/g,
|
|
3786
|
-
/"([^"]+)"\s*\.\s*localised\b/g
|
|
3787
|
-
]
|
|
3788
|
-
};
|
|
3789
|
-
PREFIX_PATTERNS = {
|
|
3790
|
-
laravel: [
|
|
3791
|
-
/\b(?:__|trans|trans_choice|Lang::(?:get|choice))\s*\(\s*'([^']*)'\s*\./g,
|
|
3792
|
-
/\b(?:__|trans|trans_choice|Lang::(?:get|choice))\s*\(\s*"([^"]*)"\s*\./g,
|
|
3793
|
-
/\b(?:__|trans|trans_choice|Lang::(?:get|choice))\s*\(\s*"([^"${]*)\{?\$/g
|
|
3794
|
-
],
|
|
3795
|
-
"js-i18n": [
|
|
3796
|
-
/(?:\$t|i18n\.t|i18next\.t)\s*\(\s*'([^']*)'\s*\+/g,
|
|
3797
|
-
/(?:\$t|i18n\.t|i18next\.t)\s*\(\s*"([^"]*)"\s*\+/g,
|
|
3798
|
-
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*'([^']*)'\s*\+/g,
|
|
3799
|
-
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*"([^"]*)"\s*\+/g,
|
|
3800
|
-
/(?:\$t|i18n\.t|i18next\.t)\s*\(\s*`([^`$]*)\$\{/g,
|
|
3801
|
-
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$]*)\$\{/g
|
|
3802
|
-
]
|
|
3803
|
-
};
|
|
3804
|
-
CACHE_VERSION = 6;
|
|
3805
|
-
EXT_SCANNER = {
|
|
3806
|
-
".php": "laravel",
|
|
3807
|
-
".vue": "js-i18n",
|
|
3808
|
-
".js": "js-i18n",
|
|
3809
|
-
".ts": "js-i18n",
|
|
3810
|
-
".jsx": "js-i18n",
|
|
3811
|
-
".tsx": "js-i18n",
|
|
3812
|
-
".mjs": "js-i18n",
|
|
3813
|
-
".cjs": "js-i18n",
|
|
3814
|
-
".dart": "flutter",
|
|
3815
|
-
".py": "gettext",
|
|
3816
|
-
".c": "gettext",
|
|
3817
|
-
".cpp": "gettext",
|
|
3818
|
-
".h": "gettext",
|
|
3819
|
-
".swift": "apple",
|
|
3820
|
-
".m": "apple",
|
|
3821
|
-
".mm": "apple"
|
|
3822
|
-
};
|
|
3823
|
-
ALWAYS_EXCLUDE = /* @__PURE__ */ new Set([
|
|
3824
|
-
"node_modules",
|
|
3825
|
-
".git",
|
|
3826
|
-
".glotfile",
|
|
3827
|
-
".claude",
|
|
3828
|
-
"dist",
|
|
3829
|
-
"build",
|
|
3830
|
-
"vendor",
|
|
3831
|
-
"coverage",
|
|
3832
|
-
".next",
|
|
3833
|
-
".nuxt",
|
|
3834
|
-
".turbo",
|
|
3835
|
-
"__pycache__"
|
|
3836
|
-
]);
|
|
3837
|
-
FLUTTER_ACCESSOR_DEFAULTS = ["l10n", "loc", "localizations", "translations"];
|
|
3838
|
-
KEY_SHAPE = /^[A-Za-z0-9_][A-Za-z0-9_/-]*(?:\.(?:[A-Za-z0-9_-]+|%[sd]))+\.?$/;
|
|
3839
|
-
STRING_LITERALS = [
|
|
3840
|
-
/'([^'\\\n]+)'/g,
|
|
3841
|
-
/"([^"\\\n]+)"/g,
|
|
3842
|
-
/`([^`\\\n]+)`/g
|
|
3843
|
-
];
|
|
3844
|
-
}
|
|
3845
|
-
});
|
|
3846
|
-
|
|
3847
|
-
// src/server/ai/context.ts
|
|
3848
|
-
import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
|
|
3849
|
-
import { resolve as resolve7 } from "path";
|
|
3850
|
-
function globToRegExp2(glob) {
|
|
3851
|
-
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3852
|
-
return new RegExp(`^${escaped}$`);
|
|
3853
|
-
}
|
|
3854
|
-
function extractSnippets(refs, projectRoot, fileCache) {
|
|
3855
|
-
const filtered = refs.filter((r) => !EXCLUDED_DIRS.some((d) => r.file.startsWith(d)));
|
|
3856
|
-
const sorted = [...filtered].sort((a, b) => a.file.length - b.file.length);
|
|
3857
|
-
const selected = sorted.slice(0, MAX_SNIPPETS);
|
|
3858
|
-
const extraRefs = filtered.length > MAX_SNIPPETS ? filtered.length - MAX_SNIPPETS : 0;
|
|
3859
|
-
const snippets = [];
|
|
3860
|
-
for (const ref of selected) {
|
|
3861
|
-
const absPath = resolve7(projectRoot, ref.file);
|
|
3862
|
-
if (!fileCache.has(ref.file)) {
|
|
3863
|
-
if (!existsSync9(absPath)) continue;
|
|
3864
|
-
const content = readFileSync10(absPath, "utf8");
|
|
3865
|
-
fileCache.set(ref.file, content.split("\n"));
|
|
3866
|
-
}
|
|
3867
|
-
const lines = fileCache.get(ref.file);
|
|
3868
|
-
const start = Math.max(0, ref.line - 1 - SNIPPET_WINDOW);
|
|
3869
|
-
const end = Math.min(lines.length, ref.line + SNIPPET_WINDOW);
|
|
3870
|
-
snippets.push({
|
|
3871
|
-
file: ref.file,
|
|
3872
|
-
startLine: start + 1,
|
|
3873
|
-
lines: lines.slice(start, end).join("\n"),
|
|
3874
|
-
scanner: ref.scanner,
|
|
3875
|
-
...snippets.length === 0 && extraRefs > 0 ? { extraRefs } : {}
|
|
3876
|
-
});
|
|
3877
|
-
}
|
|
3878
|
-
return snippets;
|
|
3879
|
-
}
|
|
3880
|
-
function buildUsageIndex(cache2) {
|
|
3881
|
-
const index = /* @__PURE__ */ new Map();
|
|
3882
|
-
for (const [file, entry] of Object.entries(cache2.files)) {
|
|
3883
|
-
for (const ref of entry.refs) {
|
|
3884
|
-
const existing = index.get(ref.key) ?? [];
|
|
3885
|
-
existing.push({ key: ref.key, file, line: ref.line, col: ref.col, scanner: ref.scanner });
|
|
3886
|
-
index.set(ref.key, existing);
|
|
3887
|
-
}
|
|
3888
|
-
}
|
|
3889
|
-
return index;
|
|
3890
|
-
}
|
|
3891
|
-
function selectContextTargets(state, opts, cache2, lastRunAt) {
|
|
3892
|
-
const cutoff = opts.all ? void 0 : opts.since ?? lastRunAt;
|
|
3893
|
-
const keyRe = opts.keyGlob ? globToRegExp2(opts.keyGlob) : null;
|
|
3894
|
-
const keySet = opts.keys ? new Set(opts.keys) : null;
|
|
3895
|
-
const usageIndex = buildUsageIndex(cache2);
|
|
3896
|
-
let candidates = [];
|
|
3897
|
-
for (const key of Object.keys(state.keys).sort()) {
|
|
3898
|
-
const entry = state.keys[key];
|
|
3899
|
-
if (entry.context && !opts.force) continue;
|
|
3900
|
-
if (keySet && !keySet.has(key)) continue;
|
|
3901
|
-
if (keyRe && !keyRe.test(key)) continue;
|
|
3902
|
-
if (cutoff) {
|
|
3903
|
-
if (!entry.createdAt) continue;
|
|
3904
|
-
if (entry.createdAt < cutoff) continue;
|
|
3905
|
-
}
|
|
3906
|
-
const source = entry.values[state.config.sourceLocale]?.value ?? "";
|
|
3907
|
-
candidates.push({ id: String(candidates.length), key, source, usageSnippets: [] });
|
|
3908
|
-
}
|
|
3909
|
-
candidates.sort((a, b) => {
|
|
3910
|
-
const ta = state.keys[a.key].createdAt ?? "";
|
|
3911
|
-
const tb = state.keys[b.key].createdAt ?? "";
|
|
3912
|
-
return tb.localeCompare(ta);
|
|
3913
|
-
});
|
|
3914
|
-
if (opts.limit !== void 0) candidates = candidates.slice(0, opts.limit);
|
|
3915
|
-
candidates.forEach((c, i) => {
|
|
3916
|
-
c.id = String(i);
|
|
3917
|
-
});
|
|
3918
|
-
return candidates;
|
|
3919
|
-
}
|
|
3920
|
-
function buildContextSystemPrompt() {
|
|
3921
|
-
return [
|
|
3922
|
-
"You are a localization context writer for a UI string catalog.",
|
|
3923
|
-
"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.",
|
|
3924
|
-
"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.",
|
|
3925
|
-
"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?",
|
|
3926
|
-
"Rules:",
|
|
3927
|
-
"- Use the code snippets as your primary signal. Look at the component name, surrounding labels, event handlers, and variable names.",
|
|
3928
|
-
"- Do NOT restate the source string itself.",
|
|
3929
|
-
"- Do NOT say 'This string is...' \u2014 write the context as a direct description.",
|
|
3930
|
-
"- Keep it under 500 characters.",
|
|
3931
|
-
"- If no code snippets are available, infer from the key path and source value."
|
|
3932
|
-
].join("\n");
|
|
3933
|
-
}
|
|
3934
|
-
function buildContextBatchPrompt(reqs) {
|
|
3935
|
-
const items = reqs.map((r) => {
|
|
3936
|
-
const snippetText = r.usageSnippets.length > 0 ? r.usageSnippets.map((s) => {
|
|
3937
|
-
const extra = s.extraRefs ? ` (and ${s.extraRefs} more call site${s.extraRefs > 1 ? "s" : ""} not shown)` : "";
|
|
3938
|
-
return `File: ${s.file} (lines ${s.startLine}+, scanner: ${s.scanner})${extra}
|
|
3939
|
-
\`\`\`
|
|
3940
|
-
${s.lines}
|
|
3941
|
-
\`\`\``;
|
|
3942
|
-
}).join("\n\n") : "(no code references found \u2014 infer from key path and source value)";
|
|
3943
|
-
return { id: r.id, key: r.key, source: r.source, codeSnippets: snippetText };
|
|
3944
|
-
});
|
|
3945
|
-
return 'Write a context note for each key. Return JSON {"items":[{"id","context"}]}.\n' + JSON.stringify(items, null, 2);
|
|
3946
|
-
}
|
|
3947
|
-
function applyContext(state, reqs, results, clock = systemClock, force = false) {
|
|
3948
|
-
const byId = new Map(reqs.map((r) => [r.id, r]));
|
|
3949
|
-
let written = 0;
|
|
3950
|
-
const errors = [];
|
|
3951
|
-
for (const res of results) {
|
|
3952
|
-
const req = byId.get(res.id);
|
|
3953
|
-
if (!req) continue;
|
|
3954
|
-
if (res.error) {
|
|
3955
|
-
errors.push({ key: req.key, error: res.error });
|
|
3956
|
-
continue;
|
|
3957
|
-
}
|
|
3958
|
-
const context = res.context?.trim() ?? "";
|
|
3959
|
-
if (!context) {
|
|
3960
|
-
errors.push({ key: req.key, error: "AI returned empty context" });
|
|
3961
|
-
continue;
|
|
3962
|
-
}
|
|
3963
|
-
if (context.length > MAX_CONTEXT_LENGTH) {
|
|
3964
|
-
errors.push({ key: req.key, error: `Context too long (${context.length} chars, max ${MAX_CONTEXT_LENGTH})` });
|
|
3965
|
-
continue;
|
|
3966
|
-
}
|
|
3967
|
-
const entry = state.keys[req.key];
|
|
3968
|
-
if (!entry || entry.context && !force) continue;
|
|
3969
|
-
entry.context = context;
|
|
3970
|
-
entry.contextSource = "ai";
|
|
3971
|
-
entry.contextAt = clock();
|
|
3972
|
-
written++;
|
|
3973
|
-
}
|
|
3974
|
-
return { written, errors };
|
|
3975
|
-
}
|
|
3976
|
-
var MAX_CONTEXT_LENGTH, SNIPPET_WINDOW, MAX_SNIPPETS, EXCLUDED_DIRS, CONTEXT_BATCH_SCHEMA;
|
|
3977
|
-
var init_context = __esm({
|
|
3978
|
-
"src/server/ai/context.ts"() {
|
|
3979
|
-
"use strict";
|
|
3980
|
-
init_state();
|
|
3981
|
-
MAX_CONTEXT_LENGTH = 500;
|
|
3982
|
-
SNIPPET_WINDOW = 15;
|
|
3983
|
-
MAX_SNIPPETS = 3;
|
|
3984
|
-
EXCLUDED_DIRS = ["node_modules/", "vendor/", "dist/", ".git/", ".glotfile/"];
|
|
3985
|
-
CONTEXT_BATCH_SCHEMA = {
|
|
3986
|
-
type: "object",
|
|
3987
|
-
properties: {
|
|
3988
|
-
items: {
|
|
3989
|
-
type: "array",
|
|
3990
|
-
items: {
|
|
3991
|
-
type: "object",
|
|
3992
|
-
properties: {
|
|
3993
|
-
id: { type: "string" },
|
|
3994
|
-
context: { type: "string" },
|
|
3995
|
-
error: { type: "string" }
|
|
3996
|
-
},
|
|
3997
|
-
required: ["id"],
|
|
3998
|
-
additionalProperties: false
|
|
3999
|
-
}
|
|
4000
|
-
}
|
|
4001
|
-
},
|
|
4002
|
-
required: ["items"],
|
|
4003
|
-
additionalProperties: false
|
|
4069
|
+
cache2.files[relPath] = prev;
|
|
4070
|
+
continue;
|
|
4071
|
+
}
|
|
4072
|
+
let content;
|
|
4073
|
+
try {
|
|
4074
|
+
content = readFileSync11(abs, "utf8");
|
|
4075
|
+
} catch {
|
|
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"
|
|
4004
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
|
+
];
|
|
4005
4186
|
}
|
|
4006
4187
|
});
|
|
4007
4188
|
|
|
@@ -4377,7 +4558,7 @@ var init_run2 = __esm({
|
|
|
4377
4558
|
});
|
|
4378
4559
|
|
|
4379
4560
|
// src/server/lint/outputs.ts
|
|
4380
|
-
import { readFileSync as
|
|
4561
|
+
import { readFileSync as readFileSync12, existsSync as existsSync11 } from "fs";
|
|
4381
4562
|
import { resolve as resolve8 } from "path";
|
|
4382
4563
|
function checkOutputs(state, root) {
|
|
4383
4564
|
const out = [];
|
|
@@ -4385,7 +4566,7 @@ function checkOutputs(state, root) {
|
|
|
4385
4566
|
const result = getAdapter(output.adapter).export(state, output);
|
|
4386
4567
|
for (const file of result.files) {
|
|
4387
4568
|
const abs = resolve8(root, file.path);
|
|
4388
|
-
const current =
|
|
4569
|
+
const current = existsSync11(abs) ? readFileSync12(abs, "utf8") : null;
|
|
4389
4570
|
if (current === null) {
|
|
4390
4571
|
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
4391
4572
|
} else if (current !== file.contents) {
|
|
@@ -4430,8 +4611,8 @@ var init_accept = __esm({
|
|
|
4430
4611
|
});
|
|
4431
4612
|
|
|
4432
4613
|
// src/server/import/detect.ts
|
|
4433
|
-
import { existsSync as
|
|
4434
|
-
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";
|
|
4435
4616
|
function safeIsDir(p) {
|
|
4436
4617
|
try {
|
|
4437
4618
|
return statSync3(p).isDirectory();
|
|
@@ -4440,7 +4621,7 @@ function safeIsDir(p) {
|
|
|
4440
4621
|
}
|
|
4441
4622
|
}
|
|
4442
4623
|
function listDirs(dir) {
|
|
4443
|
-
return readdirSync4(dir).filter((e) => safeIsDir(
|
|
4624
|
+
return readdirSync4(dir).filter((e) => safeIsDir(join6(dir, e)));
|
|
4444
4625
|
}
|
|
4445
4626
|
function fileCount(dir) {
|
|
4446
4627
|
try {
|
|
@@ -4454,23 +4635,23 @@ function pickSource(locales, sizeOf) {
|
|
|
4454
4635
|
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
4455
4636
|
}
|
|
4456
4637
|
function detectLaravel(root) {
|
|
4457
|
-
const localeRoot = [
|
|
4638
|
+
const localeRoot = [join6(root, "resources", "lang"), join6(root, "lang")].find(safeIsDir);
|
|
4458
4639
|
if (!localeRoot) return null;
|
|
4459
4640
|
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
4460
4641
|
if (locales.length === 0) return null;
|
|
4461
|
-
const sourceLocale = pickSource(locales, (loc) => fileCount(
|
|
4642
|
+
const sourceLocale = pickSource(locales, (loc) => fileCount(join6(localeRoot, loc)));
|
|
4462
4643
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
4463
4644
|
}
|
|
4464
4645
|
function detectVue(root, forced = false) {
|
|
4465
4646
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
4466
|
-
const localeRoot =
|
|
4647
|
+
const localeRoot = join6(root, rel);
|
|
4467
4648
|
if (!safeIsDir(localeRoot)) continue;
|
|
4468
4649
|
const locales = readdirSync4(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4469
4650
|
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
4470
4651
|
if (enough) {
|
|
4471
4652
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4472
4653
|
try {
|
|
4473
|
-
return statSync3(
|
|
4654
|
+
return statSync3(join6(localeRoot, `${loc}.json`)).size;
|
|
4474
4655
|
} catch {
|
|
4475
4656
|
return 0;
|
|
4476
4657
|
}
|
|
@@ -4482,7 +4663,7 @@ function detectVue(root, forced = false) {
|
|
|
4482
4663
|
}
|
|
4483
4664
|
function detectArb(root) {
|
|
4484
4665
|
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
4485
|
-
const localeRoot =
|
|
4666
|
+
const localeRoot = join6(root, rel);
|
|
4486
4667
|
if (!safeIsDir(localeRoot)) continue;
|
|
4487
4668
|
const locales = readdirSync4(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
4488
4669
|
if (locales.length >= 1) {
|
|
@@ -4492,10 +4673,10 @@ function detectArb(root) {
|
|
|
4492
4673
|
return null;
|
|
4493
4674
|
}
|
|
4494
4675
|
function lprojLocales(dir) {
|
|
4495
|
-
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")));
|
|
4496
4677
|
}
|
|
4497
4678
|
function detectApple(root) {
|
|
4498
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
4679
|
+
const candidates = [root, ...listDirs(root).map((d) => join6(root, d))];
|
|
4499
4680
|
let best = null;
|
|
4500
4681
|
for (const dir of candidates) {
|
|
4501
4682
|
const locales = lprojLocales(dir);
|
|
@@ -4507,7 +4688,7 @@ function detectApple(root) {
|
|
|
4507
4688
|
locales,
|
|
4508
4689
|
sourceLocale: pickSource(locales, (loc) => {
|
|
4509
4690
|
try {
|
|
4510
|
-
return statSync3(
|
|
4691
|
+
return statSync3(join6(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
4511
4692
|
} catch {
|
|
4512
4693
|
return 0;
|
|
4513
4694
|
}
|
|
@@ -4519,7 +4700,7 @@ function detectApple(root) {
|
|
|
4519
4700
|
}
|
|
4520
4701
|
function detectAngularXliff(root) {
|
|
4521
4702
|
for (const rel of ANGULAR_DIR_CANDIDATES) {
|
|
4522
|
-
const localeRoot = rel === "." ? root :
|
|
4703
|
+
const localeRoot = rel === "." ? root : join6(root, rel);
|
|
4523
4704
|
if (!safeIsDir(localeRoot)) continue;
|
|
4524
4705
|
const files = readdirSync4(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
|
|
4525
4706
|
if (files.length === 0) continue;
|
|
@@ -4527,7 +4708,7 @@ function detectAngularXliff(root) {
|
|
|
4527
4708
|
const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
|
|
4528
4709
|
let sourceLocale;
|
|
4529
4710
|
try {
|
|
4530
|
-
sourceLocale =
|
|
4711
|
+
sourceLocale = readFileSync13(join6(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
|
|
4531
4712
|
} catch {
|
|
4532
4713
|
}
|
|
4533
4714
|
if (!sourceLocale && locales.length === 0) continue;
|
|
@@ -4538,14 +4719,14 @@ function detectAngularXliff(root) {
|
|
|
4538
4719
|
return null;
|
|
4539
4720
|
}
|
|
4540
4721
|
function detectRails(root) {
|
|
4541
|
-
const localeRoot =
|
|
4722
|
+
const localeRoot = join6(root, "config", "locales");
|
|
4542
4723
|
if (!safeIsDir(localeRoot)) return null;
|
|
4543
4724
|
const locales = [];
|
|
4544
4725
|
for (const file of readdirSync4(localeRoot).sort()) {
|
|
4545
4726
|
if (!/\.ya?ml$/.test(file)) continue;
|
|
4546
4727
|
let text;
|
|
4547
4728
|
try {
|
|
4548
|
-
text =
|
|
4729
|
+
text = readFileSync13(join6(localeRoot, file), "utf8");
|
|
4549
4730
|
} catch {
|
|
4550
4731
|
continue;
|
|
4551
4732
|
}
|
|
@@ -4559,15 +4740,15 @@ function detectRails(root) {
|
|
|
4559
4740
|
}
|
|
4560
4741
|
function detectI18next(root) {
|
|
4561
4742
|
for (const rel of I18NEXT_DIR_CANDIDATES) {
|
|
4562
|
-
const localeRoot =
|
|
4743
|
+
const localeRoot = join6(root, rel);
|
|
4563
4744
|
if (!safeIsDir(localeRoot)) continue;
|
|
4564
4745
|
const locales = listDirs(localeRoot).filter(
|
|
4565
|
-
(d) => LOCALE_RE.test(d) && readdirSync4(
|
|
4746
|
+
(d) => LOCALE_RE.test(d) && readdirSync4(join6(localeRoot, d)).some((f) => f.endsWith(".json"))
|
|
4566
4747
|
);
|
|
4567
4748
|
if (locales.length === 0) continue;
|
|
4568
4749
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4569
4750
|
try {
|
|
4570
|
-
return readdirSync4(
|
|
4751
|
+
return readdirSync4(join6(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync3(join6(localeRoot, loc, f)).size, 0);
|
|
4571
4752
|
} catch {
|
|
4572
4753
|
return 0;
|
|
4573
4754
|
}
|
|
@@ -4584,8 +4765,8 @@ function gettextLocales(dir) {
|
|
|
4584
4765
|
if (!locales.includes(flat)) locales.push(flat);
|
|
4585
4766
|
continue;
|
|
4586
4767
|
}
|
|
4587
|
-
if (!LOCALE_RE.test(entry) || !safeIsDir(
|
|
4588
|
-
const sub =
|
|
4768
|
+
if (!LOCALE_RE.test(entry) || !safeIsDir(join6(dir, entry))) continue;
|
|
4769
|
+
const sub = join6(dir, entry);
|
|
4589
4770
|
const hasPo = (d) => {
|
|
4590
4771
|
try {
|
|
4591
4772
|
return readdirSync4(d).some((f) => f.endsWith(".po"));
|
|
@@ -4593,7 +4774,7 @@ function gettextLocales(dir) {
|
|
|
4593
4774
|
return false;
|
|
4594
4775
|
}
|
|
4595
4776
|
};
|
|
4596
|
-
if (hasPo(
|
|
4777
|
+
if (hasPo(join6(sub, "LC_MESSAGES")) || hasPo(sub)) {
|
|
4597
4778
|
if (!locales.includes(entry)) locales.push(entry);
|
|
4598
4779
|
}
|
|
4599
4780
|
}
|
|
@@ -4601,7 +4782,7 @@ function gettextLocales(dir) {
|
|
|
4601
4782
|
}
|
|
4602
4783
|
function detectGettext(root) {
|
|
4603
4784
|
for (const rel of GETTEXT_DIR_CANDIDATES) {
|
|
4604
|
-
const localeRoot =
|
|
4785
|
+
const localeRoot = join6(root, rel);
|
|
4605
4786
|
if (!safeIsDir(localeRoot)) continue;
|
|
4606
4787
|
const locales = gettextLocales(localeRoot);
|
|
4607
4788
|
if (locales.length === 0) continue;
|
|
@@ -4610,10 +4791,10 @@ function detectGettext(root) {
|
|
|
4610
4791
|
return null;
|
|
4611
4792
|
}
|
|
4612
4793
|
function detectAppleStringsdict(root) {
|
|
4613
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
4794
|
+
const candidates = [root, ...listDirs(root).map((d) => join6(root, d))];
|
|
4614
4795
|
let best = null;
|
|
4615
4796
|
for (const dir of candidates) {
|
|
4616
|
-
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")));
|
|
4617
4798
|
if (locales.length === 0) continue;
|
|
4618
4799
|
if (!best || locales.length > best.locales.length) {
|
|
4619
4800
|
best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
@@ -4622,7 +4803,7 @@ function detectAppleStringsdict(root) {
|
|
|
4622
4803
|
return best;
|
|
4623
4804
|
}
|
|
4624
4805
|
function detect(root, formatOverride) {
|
|
4625
|
-
if (!
|
|
4806
|
+
if (!existsSync12(root)) return null;
|
|
4626
4807
|
if (formatOverride) {
|
|
4627
4808
|
const fn = BY_FORMAT[formatOverride];
|
|
4628
4809
|
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
@@ -4696,8 +4877,8 @@ var init_flatten = __esm({
|
|
|
4696
4877
|
});
|
|
4697
4878
|
|
|
4698
4879
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
4699
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
4700
|
-
import { join as
|
|
4880
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync14 } from "fs";
|
|
4881
|
+
import { join as join7 } from "path";
|
|
4701
4882
|
var LOCALE_RE2, vueI18nJson2;
|
|
4702
4883
|
var init_vue_i18n_json2 = __esm({
|
|
4703
4884
|
"src/server/import/parsers/vue-i18n-json.ts"() {
|
|
@@ -4717,7 +4898,7 @@ var init_vue_i18n_json2 = __esm({
|
|
|
4717
4898
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4718
4899
|
let data;
|
|
4719
4900
|
try {
|
|
4720
|
-
data = JSON.parse(
|
|
4901
|
+
data = JSON.parse(readFileSync14(join7(localeRoot, file), "utf8"));
|
|
4721
4902
|
} catch (e) {
|
|
4722
4903
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
4723
4904
|
continue;
|
|
@@ -4745,16 +4926,16 @@ var init_placeholders2 = __esm({
|
|
|
4745
4926
|
|
|
4746
4927
|
// src/server/import/parsers/laravel-php.ts
|
|
4747
4928
|
import { readdirSync as readdirSync6, statSync as statSync4 } from "fs";
|
|
4748
|
-
import { join as
|
|
4929
|
+
import { join as join8, relative as relative2 } from "path";
|
|
4749
4930
|
import { execFileSync } from "child_process";
|
|
4750
4931
|
function listDirs2(dir) {
|
|
4751
|
-
return readdirSync6(dir).filter((e) => statSync4(
|
|
4932
|
+
return readdirSync6(dir).filter((e) => statSync4(join8(dir, e)).isDirectory());
|
|
4752
4933
|
}
|
|
4753
4934
|
function listPhpFiles(dir) {
|
|
4754
4935
|
const out = [];
|
|
4755
4936
|
const walk = (d) => {
|
|
4756
4937
|
for (const e of readdirSync6(d)) {
|
|
4757
|
-
const full =
|
|
4938
|
+
const full = join8(d, e);
|
|
4758
4939
|
if (statSync4(full).isDirectory()) walk(full);
|
|
4759
4940
|
else if (e.endsWith(".php")) out.push(full);
|
|
4760
4941
|
}
|
|
@@ -4797,7 +4978,7 @@ var init_laravel_php2 = __esm({
|
|
|
4797
4978
|
for (const locale of listDirs2(localeRoot).sort()) {
|
|
4798
4979
|
if (locale === "vendor") continue;
|
|
4799
4980
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4800
|
-
const localeDir =
|
|
4981
|
+
const localeDir = join8(localeRoot, locale);
|
|
4801
4982
|
locales.push(locale);
|
|
4802
4983
|
for (const file of listPhpFiles(localeDir)) {
|
|
4803
4984
|
const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
|
|
@@ -4822,8 +5003,8 @@ var init_laravel_php2 = __esm({
|
|
|
4822
5003
|
});
|
|
4823
5004
|
|
|
4824
5005
|
// src/server/import/parsers/flutter-arb.ts
|
|
4825
|
-
import { readdirSync as readdirSync7, readFileSync as
|
|
4826
|
-
import { join as
|
|
5006
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync15 } from "fs";
|
|
5007
|
+
import { join as join9 } from "path";
|
|
4827
5008
|
function localeFromArbName(file) {
|
|
4828
5009
|
const m = file.match(/^(.+)\.arb$/);
|
|
4829
5010
|
if (!m) return null;
|
|
@@ -4863,7 +5044,7 @@ var init_flutter_arb2 = __esm({
|
|
|
4863
5044
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4864
5045
|
let data;
|
|
4865
5046
|
try {
|
|
4866
|
-
data = JSON.parse(
|
|
5047
|
+
data = JSON.parse(readFileSync15(join9(localeRoot, file), "utf8"));
|
|
4867
5048
|
} catch (e) {
|
|
4868
5049
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
4869
5050
|
continue;
|
|
@@ -4890,8 +5071,8 @@ var init_flutter_arb2 = __esm({
|
|
|
4890
5071
|
});
|
|
4891
5072
|
|
|
4892
5073
|
// src/server/import/parsers/apple-strings.ts
|
|
4893
|
-
import { readdirSync as readdirSync8, readFileSync as
|
|
4894
|
-
import { join as
|
|
5074
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync16, statSync as statSync5 } from "fs";
|
|
5075
|
+
import { join as join10 } from "path";
|
|
4895
5076
|
function localeFromLproj(dir) {
|
|
4896
5077
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
4897
5078
|
if (!m) return null;
|
|
@@ -4999,16 +5180,16 @@ var init_apple_strings2 = __esm({
|
|
|
4999
5180
|
const locale = localeFromLproj(dir);
|
|
5000
5181
|
if (!locale) continue;
|
|
5001
5182
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5002
|
-
const file =
|
|
5183
|
+
const file = join10(localeRoot, dir, TABLE);
|
|
5003
5184
|
let text;
|
|
5004
5185
|
try {
|
|
5005
5186
|
if (!statSync5(file).isFile()) continue;
|
|
5006
|
-
text =
|
|
5187
|
+
text = readFileSync16(file, "utf8");
|
|
5007
5188
|
} catch {
|
|
5008
5189
|
continue;
|
|
5009
5190
|
}
|
|
5010
5191
|
locales.push(locale);
|
|
5011
|
-
const others = readdirSync8(
|
|
5192
|
+
const others = readdirSync8(join10(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
|
|
5012
5193
|
if (others.length) {
|
|
5013
5194
|
warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
|
|
5014
5195
|
}
|
|
@@ -5023,8 +5204,8 @@ var init_apple_strings2 = __esm({
|
|
|
5023
5204
|
});
|
|
5024
5205
|
|
|
5025
5206
|
// src/server/import/parsers/angular-xliff.ts
|
|
5026
|
-
import { readdirSync as readdirSync9, readFileSync as
|
|
5027
|
-
import { join as
|
|
5207
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync17 } from "fs";
|
|
5208
|
+
import { join as join11 } from "path";
|
|
5028
5209
|
function decodeEntities(s) {
|
|
5029
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, "&");
|
|
5030
5211
|
}
|
|
@@ -5076,7 +5257,7 @@ var init_angular_xliff2 = __esm({
|
|
|
5076
5257
|
if (fnameLocale !== void 0 && !LOCALE_RE5.test(fnameLocale)) continue;
|
|
5077
5258
|
let xml;
|
|
5078
5259
|
try {
|
|
5079
|
-
xml =
|
|
5260
|
+
xml = readFileSync17(join11(localeRoot, file), "utf8");
|
|
5080
5261
|
} catch (e) {
|
|
5081
5262
|
warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
|
|
5082
5263
|
continue;
|
|
@@ -5119,8 +5300,8 @@ var init_angular_xliff2 = __esm({
|
|
|
5119
5300
|
});
|
|
5120
5301
|
|
|
5121
5302
|
// src/server/import/parsers/gettext-po.ts
|
|
5122
|
-
import { readdirSync as readdirSync10, readFileSync as
|
|
5123
|
-
import { join as
|
|
5303
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync18 } from "fs";
|
|
5304
|
+
import { join as join12 } from "path";
|
|
5124
5305
|
function unescapePo(s) {
|
|
5125
5306
|
return s.replace(
|
|
5126
5307
|
/\\([\\"ntr])/g,
|
|
@@ -5191,17 +5372,17 @@ function discoverPoFiles(root) {
|
|
|
5191
5372
|
for (const e of entries) {
|
|
5192
5373
|
if (e.isFile() && e.name.endsWith(".po")) {
|
|
5193
5374
|
const base = e.name.slice(0, -3);
|
|
5194
|
-
found.push({ path:
|
|
5375
|
+
found.push({ path: join12(root, e.name), rel: e.name, locale: LOCALE_RE6.test(base) ? base : null });
|
|
5195
5376
|
} else if (e.isDirectory() && LOCALE_RE6.test(e.name)) {
|
|
5196
|
-
for (const sub of [
|
|
5377
|
+
for (const sub of [join12(e.name, "LC_MESSAGES"), e.name]) {
|
|
5197
5378
|
let names;
|
|
5198
5379
|
try {
|
|
5199
|
-
names = readdirSync10(
|
|
5380
|
+
names = readdirSync10(join12(root, sub)).sort();
|
|
5200
5381
|
} catch {
|
|
5201
5382
|
continue;
|
|
5202
5383
|
}
|
|
5203
5384
|
for (const f of names) {
|
|
5204
|
-
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 });
|
|
5205
5386
|
}
|
|
5206
5387
|
}
|
|
5207
5388
|
}
|
|
@@ -5225,7 +5406,7 @@ var init_gettext_po2 = __esm({
|
|
|
5225
5406
|
for (const file of discoverPoFiles(localeRoot)) {
|
|
5226
5407
|
let entries;
|
|
5227
5408
|
try {
|
|
5228
|
-
entries = parseEntries(
|
|
5409
|
+
entries = parseEntries(readFileSync18(file.path, "utf8"));
|
|
5229
5410
|
} catch (e) {
|
|
5230
5411
|
warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
|
|
5231
5412
|
continue;
|
|
@@ -5272,8 +5453,8 @@ var init_gettext_po2 = __esm({
|
|
|
5272
5453
|
});
|
|
5273
5454
|
|
|
5274
5455
|
// src/server/import/parsers/i18next-json.ts
|
|
5275
|
-
import { readdirSync as readdirSync11, readFileSync as
|
|
5276
|
-
import { join as
|
|
5456
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync19, statSync as statSync6 } from "fs";
|
|
5457
|
+
import { join as join13 } from "path";
|
|
5277
5458
|
function safeIsDir2(p) {
|
|
5278
5459
|
try {
|
|
5279
5460
|
return statSync6(p).isDirectory();
|
|
@@ -5288,7 +5469,7 @@ function fromI18next(value) {
|
|
|
5288
5469
|
function ingestFile(path, label, prefix, locale, keys, warnings) {
|
|
5289
5470
|
let data;
|
|
5290
5471
|
try {
|
|
5291
|
-
data = JSON.parse(
|
|
5472
|
+
data = JSON.parse(readFileSync19(path, "utf8"));
|
|
5292
5473
|
} catch (e) {
|
|
5293
5474
|
warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
|
|
5294
5475
|
return false;
|
|
@@ -5341,7 +5522,7 @@ var init_i18next_json2 = __esm({
|
|
|
5341
5522
|
const keys = {};
|
|
5342
5523
|
const locales = [];
|
|
5343
5524
|
for (const entry of readdirSync11(localeRoot).sort()) {
|
|
5344
|
-
const full =
|
|
5525
|
+
const full = join13(localeRoot, entry);
|
|
5345
5526
|
if (safeIsDir2(full)) {
|
|
5346
5527
|
if (!LOCALE_RE7.test(entry)) continue;
|
|
5347
5528
|
if (opts?.locales && !opts.locales.includes(entry)) continue;
|
|
@@ -5350,7 +5531,7 @@ var init_i18next_json2 = __esm({
|
|
|
5350
5531
|
if (!file.endsWith(".json")) continue;
|
|
5351
5532
|
const ns = file.slice(0, -".json".length);
|
|
5352
5533
|
const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
|
|
5353
|
-
if (ingestFile(
|
|
5534
|
+
if (ingestFile(join13(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
|
|
5354
5535
|
}
|
|
5355
5536
|
if (any && !locales.includes(entry)) locales.push(entry);
|
|
5356
5537
|
} else if (entry.endsWith(".json")) {
|
|
@@ -5369,8 +5550,8 @@ var init_i18next_json2 = __esm({
|
|
|
5369
5550
|
});
|
|
5370
5551
|
|
|
5371
5552
|
// src/server/import/parsers/rails-yaml.ts
|
|
5372
|
-
import { readdirSync as readdirSync12, readFileSync as
|
|
5373
|
-
import { join as
|
|
5553
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync20 } from "fs";
|
|
5554
|
+
import { join as join14 } from "path";
|
|
5374
5555
|
function fromRuby(value) {
|
|
5375
5556
|
return value.replace(/%\{(\w+)\}/g, "{$1}");
|
|
5376
5557
|
}
|
|
@@ -5587,7 +5768,7 @@ var init_rails_yaml2 = __esm({
|
|
|
5587
5768
|
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
5588
5769
|
let text;
|
|
5589
5770
|
try {
|
|
5590
|
-
text =
|
|
5771
|
+
text = readFileSync20(join14(localeRoot, file), "utf8");
|
|
5591
5772
|
} catch (e) {
|
|
5592
5773
|
warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
|
|
5593
5774
|
continue;
|
|
@@ -5610,8 +5791,8 @@ var init_rails_yaml2 = __esm({
|
|
|
5610
5791
|
});
|
|
5611
5792
|
|
|
5612
5793
|
// src/server/import/parsers/apple-stringsdict.ts
|
|
5613
|
-
import { readdirSync as readdirSync13, readFileSync as
|
|
5614
|
-
import { join as
|
|
5794
|
+
import { readdirSync as readdirSync13, readFileSync as readFileSync21, statSync as statSync7 } from "fs";
|
|
5795
|
+
import { join as join15 } from "path";
|
|
5615
5796
|
function localeFromLproj2(dir) {
|
|
5616
5797
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
5617
5798
|
if (!m) return null;
|
|
@@ -5753,16 +5934,16 @@ var init_apple_stringsdict2 = __esm({
|
|
|
5753
5934
|
const locale = localeFromLproj2(dir);
|
|
5754
5935
|
if (!locale) continue;
|
|
5755
5936
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5756
|
-
const file =
|
|
5937
|
+
const file = join15(localeRoot, dir, TABLE2);
|
|
5757
5938
|
let text;
|
|
5758
5939
|
try {
|
|
5759
5940
|
if (!statSync7(file).isFile()) continue;
|
|
5760
|
-
text =
|
|
5941
|
+
text = readFileSync21(file, "utf8");
|
|
5761
5942
|
} catch {
|
|
5762
5943
|
continue;
|
|
5763
5944
|
}
|
|
5764
5945
|
locales.push(locale);
|
|
5765
|
-
const others = readdirSync13(
|
|
5946
|
+
const others = readdirSync13(join15(localeRoot, dir)).filter(
|
|
5766
5947
|
(f) => f.endsWith(".stringsdict") && f !== TABLE2
|
|
5767
5948
|
);
|
|
5768
5949
|
if (others.length) {
|
|
@@ -6221,12 +6402,12 @@ var init_checks = __esm({
|
|
|
6221
6402
|
});
|
|
6222
6403
|
|
|
6223
6404
|
// src/server/ui-prefs.ts
|
|
6224
|
-
import { readFileSync as
|
|
6405
|
+
import { readFileSync as readFileSync22 } from "fs";
|
|
6225
6406
|
import { homedir } from "os";
|
|
6226
|
-
import { join as
|
|
6407
|
+
import { join as join16 } from "path";
|
|
6227
6408
|
function readJson2(path) {
|
|
6228
6409
|
try {
|
|
6229
|
-
const parsed = JSON.parse(
|
|
6410
|
+
const parsed = JSON.parse(readFileSync22(path, "utf8"));
|
|
6230
6411
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
6231
6412
|
} catch {
|
|
6232
6413
|
return {};
|
|
@@ -6251,7 +6432,7 @@ var init_ui_prefs = __esm({
|
|
|
6251
6432
|
THEMES = ["system", "light", "dark"];
|
|
6252
6433
|
isThemeMode = (v) => THEMES.includes(v);
|
|
6253
6434
|
isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
|
|
6254
|
-
defaultUiPrefsPath = () =>
|
|
6435
|
+
defaultUiPrefsPath = () => join16(homedir(), ".glotfile", "ui.json");
|
|
6255
6436
|
DEFAULTS = { theme: "system" };
|
|
6256
6437
|
}
|
|
6257
6438
|
});
|
|
@@ -6259,19 +6440,34 @@ var init_ui_prefs = __esm({
|
|
|
6259
6440
|
// src/server/api.ts
|
|
6260
6441
|
import { Hono } from "hono";
|
|
6261
6442
|
import { streamSSE } from "hono/streaming";
|
|
6262
|
-
import { readFileSync as
|
|
6443
|
+
import { readFileSync as readFileSync23, existsSync as existsSync13, readdirSync as readdirSync14, statSync as statSync8, rmSync as rmSync6 } from "fs";
|
|
6263
6444
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
6264
6445
|
function projectName(root) {
|
|
6265
6446
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
6266
|
-
if (
|
|
6447
|
+
if (existsSync13(nameFile)) {
|
|
6267
6448
|
try {
|
|
6268
|
-
const name =
|
|
6449
|
+
const name = readFileSync23(nameFile, "utf8").trim();
|
|
6269
6450
|
if (name) return name;
|
|
6270
6451
|
} catch {
|
|
6271
6452
|
}
|
|
6272
6453
|
}
|
|
6273
6454
|
return basename(root);
|
|
6274
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
|
+
}
|
|
6275
6471
|
function createApi(deps) {
|
|
6276
6472
|
const app = new Hono();
|
|
6277
6473
|
const load = () => loadState(deps.statePath);
|
|
@@ -6398,7 +6594,7 @@ function createApi(deps) {
|
|
|
6398
6594
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
6399
6595
|
const abs = resolve9(dir, name);
|
|
6400
6596
|
let filePath = null;
|
|
6401
|
-
if ((name === "glotfile" || name.endsWith(".glotfile")) &&
|
|
6597
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync13(resolve9(abs, "config.json"))) {
|
|
6402
6598
|
filePath = resolve9(dir, `${name}.json`);
|
|
6403
6599
|
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
6404
6600
|
filePath = abs;
|
|
@@ -6432,7 +6628,7 @@ function createApi(deps) {
|
|
|
6432
6628
|
const resolved = resolve9(projectRoot, path);
|
|
6433
6629
|
const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
|
|
6434
6630
|
if (!inside) return c.json({ error: "file is outside the project" }, 400);
|
|
6435
|
-
if (!
|
|
6631
|
+
if (!existsSync13(resolved)) return c.json({ error: "file not found" }, 400);
|
|
6436
6632
|
loadState(resolved);
|
|
6437
6633
|
deps.statePath = resolved;
|
|
6438
6634
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
@@ -6493,9 +6689,9 @@ function createApi(deps) {
|
|
|
6493
6689
|
const abs = resolve9(root, screenshot);
|
|
6494
6690
|
const rel = relative4(root, abs);
|
|
6495
6691
|
const seg0 = rel.split(sep2)[0] ?? "";
|
|
6496
|
-
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") &&
|
|
6692
|
+
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync13(abs)) {
|
|
6497
6693
|
try {
|
|
6498
|
-
|
|
6694
|
+
rmSync6(abs);
|
|
6499
6695
|
} catch {
|
|
6500
6696
|
}
|
|
6501
6697
|
}
|
|
@@ -7223,19 +7419,7 @@ function createApi(deps) {
|
|
|
7223
7419
|
return;
|
|
7224
7420
|
}
|
|
7225
7421
|
await stream.writeSSE({ event: "start", data: JSON.stringify({ total: targets.length }) });
|
|
7226
|
-
|
|
7227
|
-
for (const target of targets) {
|
|
7228
|
-
const allRefs = Object.entries(cache2.files).flatMap(
|
|
7229
|
-
([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
7230
|
-
key: r.key,
|
|
7231
|
-
file,
|
|
7232
|
-
line: r.line,
|
|
7233
|
-
col: r.col,
|
|
7234
|
-
scanner: r.scanner
|
|
7235
|
-
}))
|
|
7236
|
-
);
|
|
7237
|
-
target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
|
|
7238
|
-
}
|
|
7422
|
+
attachUsageSnippets(targets, cache2, projectRoot);
|
|
7239
7423
|
const system = buildContextSystemPrompt();
|
|
7240
7424
|
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
7241
7425
|
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
@@ -7287,6 +7471,100 @@ function createApi(deps) {
|
|
|
7287
7471
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: targets.length, written: totalWritten, errors: allErrors }) });
|
|
7288
7472
|
});
|
|
7289
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
|
+
});
|
|
7290
7568
|
app.onError(
|
|
7291
7569
|
(err, c) => c.json({ error: err.message }, err instanceof GlotfileError ? 400 : 500)
|
|
7292
7570
|
);
|
|
@@ -7312,6 +7590,8 @@ var init_api = __esm({
|
|
|
7312
7590
|
init_provider();
|
|
7313
7591
|
init_batch_run();
|
|
7314
7592
|
init_pending_batch();
|
|
7593
|
+
init_context_batch_run();
|
|
7594
|
+
init_pending_context_batch();
|
|
7315
7595
|
init_estimate();
|
|
7316
7596
|
init_pricing();
|
|
7317
7597
|
init_log();
|
|
@@ -7335,7 +7615,7 @@ __export(server_exports, {
|
|
|
7335
7615
|
import { Hono as Hono2 } from "hono";
|
|
7336
7616
|
import { serve } from "@hono/node-server";
|
|
7337
7617
|
import { fileURLToPath } from "url";
|
|
7338
|
-
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";
|
|
7339
7619
|
import { readFile, stat } from "fs/promises";
|
|
7340
7620
|
import { createServer } from "net";
|
|
7341
7621
|
import open from "open";
|
|
@@ -7378,7 +7658,7 @@ function buildApp(opts) {
|
|
|
7378
7658
|
const file = await readFileResponse(target);
|
|
7379
7659
|
if (file) return file;
|
|
7380
7660
|
}
|
|
7381
|
-
const index = await readFileResponse(
|
|
7661
|
+
const index = await readFileResponse(join17(root, "index.html"));
|
|
7382
7662
|
if (index) return index;
|
|
7383
7663
|
return c.notFound();
|
|
7384
7664
|
});
|
|
@@ -7436,7 +7716,7 @@ var init_server = __esm({
|
|
|
7436
7716
|
init_scan();
|
|
7437
7717
|
init_scanner();
|
|
7438
7718
|
here = dirname4(fileURLToPath(import.meta.url));
|
|
7439
|
-
DEFAULT_UI_DIR =
|
|
7719
|
+
DEFAULT_UI_DIR = join17(here, "..", "ui");
|
|
7440
7720
|
MIME = {
|
|
7441
7721
|
".html": "text/html; charset=utf-8",
|
|
7442
7722
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -7475,6 +7755,8 @@ init_run();
|
|
|
7475
7755
|
init_provider();
|
|
7476
7756
|
init_batch_run();
|
|
7477
7757
|
init_pending_batch();
|
|
7758
|
+
init_context_batch_run();
|
|
7759
|
+
init_pending_context_batch();
|
|
7478
7760
|
init_estimate();
|
|
7479
7761
|
init_pricing();
|
|
7480
7762
|
init_log();
|
|
@@ -7483,8 +7765,8 @@ init_scanner();
|
|
|
7483
7765
|
init_context();
|
|
7484
7766
|
init_run2();
|
|
7485
7767
|
init_outputs();
|
|
7486
|
-
import { resolve as resolve11, dirname as dirname5, join as
|
|
7487
|
-
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";
|
|
7488
7770
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7489
7771
|
|
|
7490
7772
|
// src/server/lint/locate.ts
|
|
@@ -7859,11 +8141,16 @@ async function waitAndApply(args, provider, pending, ai) {
|
|
|
7859
8141
|
async function runBatch(args) {
|
|
7860
8142
|
const projectRoot = dirname5(resolve11(args.statePath));
|
|
7861
8143
|
const pending = loadPendingBatch(projectRoot);
|
|
7862
|
-
|
|
7863
|
-
|
|
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`.");
|
|
7864
8147
|
return;
|
|
7865
8148
|
}
|
|
7866
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) {
|
|
7867
8154
|
if (action === "cancel") {
|
|
7868
8155
|
let remoteFailed = false;
|
|
7869
8156
|
try {
|
|
@@ -7900,6 +8187,53 @@ async function runBatch(args) {
|
|
|
7900
8187
|
}
|
|
7901
8188
|
await applyPending(args, provider, pending, ai);
|
|
7902
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
|
+
}
|
|
7903
8237
|
function printReport(report, format, rawText) {
|
|
7904
8238
|
if (format === "json") console.log(formatJson(report).trimEnd());
|
|
7905
8239
|
else if (format === "sarif") console.log(formatSarif(report, rawText).trimEnd());
|
|
@@ -7919,7 +8253,7 @@ async function runLintCmd(args) {
|
|
|
7919
8253
|
}
|
|
7920
8254
|
return;
|
|
7921
8255
|
}
|
|
7922
|
-
const rawText =
|
|
8256
|
+
const rawText = existsSync14(args.statePath) ? readFileSync24(args.statePath, "utf8") : "";
|
|
7923
8257
|
const report = await runLint(state, {
|
|
7924
8258
|
locales: args.locales,
|
|
7925
8259
|
ruleIds: args.ruleIds,
|
|
@@ -7943,7 +8277,7 @@ async function runCheck(args) {
|
|
|
7943
8277
|
process.exitCode = 1;
|
|
7944
8278
|
return;
|
|
7945
8279
|
}
|
|
7946
|
-
const rawText =
|
|
8280
|
+
const rawText = existsSync14(args.statePath) ? readFileSync24(args.statePath, "utf8") : "";
|
|
7947
8281
|
const root = dirname5(resolve11(args.statePath));
|
|
7948
8282
|
const lint = await runLint(state, {});
|
|
7949
8283
|
const findings = sortFindings([...lint.findings, ...checkOutputs(state, root)]);
|
|
@@ -7956,7 +8290,7 @@ async function runImportCmd(args) {
|
|
|
7956
8290
|
const { runImport: runImport2 } = await Promise.resolve().then(() => (init_run3(), run_exports));
|
|
7957
8291
|
const projectRoot = args.importSource ? resolve11(args.importSource) : dirname5(resolve11(args.statePath));
|
|
7958
8292
|
const out = resolve11(projectRoot, "glotfile.json");
|
|
7959
|
-
if (
|
|
8293
|
+
if (existsSync14(out) && !args.importForce) {
|
|
7960
8294
|
console.error(`${out} already exists; pass --force to overwrite`);
|
|
7961
8295
|
process.exitCode = 1;
|
|
7962
8296
|
return;
|
|
@@ -8023,6 +8357,32 @@ async function runBuildContext(args) {
|
|
|
8023
8357
|
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
8024
8358
|
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
8025
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
|
+
}
|
|
8026
8386
|
const chunks = [];
|
|
8027
8387
|
for (let i = 0; i < targets.length; i += batchSize) chunks.push(targets.slice(i, i + batchSize));
|
|
8028
8388
|
let written = 0;
|
|
@@ -8104,19 +8464,19 @@ function runSplit(args) {
|
|
|
8104
8464
|
`Split catalog into ${splitDirFor(args.statePath)}/ (config.json, keys.json, locales/ \u2014 up to ${state.config.locales.length} locale files). Removed ${args.statePath}.`
|
|
8105
8465
|
);
|
|
8106
8466
|
}
|
|
8107
|
-
var SKILL_SRC =
|
|
8467
|
+
var SKILL_SRC = join18(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "skill");
|
|
8108
8468
|
function runSkill(args) {
|
|
8109
8469
|
if (args.print) {
|
|
8110
|
-
console.log(
|
|
8470
|
+
console.log(readFileSync24(join18(SKILL_SRC, "SKILL.md"), "utf8").trimEnd());
|
|
8111
8471
|
return;
|
|
8112
8472
|
}
|
|
8113
8473
|
const dest = resolve11(process.cwd(), ".claude", "skills", "glotfile");
|
|
8114
|
-
if (
|
|
8474
|
+
if (existsSync14(dest) && !args.importForce) {
|
|
8115
8475
|
console.error(`${dest} already exists; pass --force to overwrite`);
|
|
8116
8476
|
process.exitCode = 1;
|
|
8117
8477
|
return;
|
|
8118
8478
|
}
|
|
8119
|
-
|
|
8479
|
+
mkdirSync6(dirname5(dest), { recursive: true });
|
|
8120
8480
|
cpSync(SKILL_SRC, dest, { recursive: true });
|
|
8121
8481
|
console.log(`Installed the glotfile skill to ${dest}. Restart Claude Code to pick it up.`);
|
|
8122
8482
|
}
|
|
@@ -8181,7 +8541,7 @@ var COMMAND_HELP = {
|
|
|
8181
8541
|
},
|
|
8182
8542
|
"build-context": {
|
|
8183
8543
|
summary: "AI-generate per-key context to improve translation (requires a prior scan).",
|
|
8184
|
-
usage: "glotfile build-context [--all] [--key <glob>] [--limit <n>] [--since <date>]",
|
|
8544
|
+
usage: "glotfile build-context [--all] [--key <glob>] [--limit <n>] [--since <date>] [--batch]",
|
|
8185
8545
|
options: [
|
|
8186
8546
|
["--all", "(Re)build context for every key, not just those missing it"],
|
|
8187
8547
|
["--key <glob>", "Only keys matching this glob"],
|
|
@@ -8256,8 +8616,8 @@ ${formatOpts([...options, ...GLOBAL_OPTS])}`);
|
|
|
8256
8616
|
);
|
|
8257
8617
|
}
|
|
8258
8618
|
function printVersion() {
|
|
8259
|
-
const pkgPath =
|
|
8260
|
-
console.log(JSON.parse(
|
|
8619
|
+
const pkgPath = join18(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
|
|
8620
|
+
console.log(JSON.parse(readFileSync24(pkgPath, "utf8")).version);
|
|
8261
8621
|
}
|
|
8262
8622
|
async function main(argv) {
|
|
8263
8623
|
const args = parseArgs(argv);
|