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/server.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Hono as Hono2 } from "hono";
|
|
3
3
|
import { serve } from "@hono/node-server";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
-
import { dirname as dirname4, join as
|
|
5
|
+
import { dirname as dirname4, join as join17, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
6
6
|
import { readFile, stat } from "fs/promises";
|
|
7
7
|
import { createServer } from "net";
|
|
8
8
|
import open from "open";
|
|
@@ -2841,7 +2841,7 @@ function checkOutputs(state, root) {
|
|
|
2841
2841
|
}
|
|
2842
2842
|
|
|
2843
2843
|
// src/server/api.ts
|
|
2844
|
-
import { readFileSync as
|
|
2844
|
+
import { readFileSync as readFileSync23, existsSync as existsSync13, readdirSync as readdirSync14, statSync as statSync8, rmSync as rmSync6 } from "fs";
|
|
2845
2845
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
2846
2846
|
|
|
2847
2847
|
// src/server/ai/anthropic.ts
|
|
@@ -2851,6 +2851,9 @@ import Anthropic from "@anthropic-ai/sdk";
|
|
|
2851
2851
|
function supportsBatchTranslate(p) {
|
|
2852
2852
|
return typeof p.submitTranslationBatch === "function";
|
|
2853
2853
|
}
|
|
2854
|
+
function supportsBatchComplete(p) {
|
|
2855
|
+
return typeof p.submitCompletionBatch === "function";
|
|
2856
|
+
}
|
|
2854
2857
|
function buildSystemPrompt(hasPluralItems) {
|
|
2855
2858
|
const lines = [
|
|
2856
2859
|
"You are a professional software localization engine for a UI string catalog.",
|
|
@@ -3082,6 +3085,67 @@ async function runBatched(reqs, batchSize, callBatch, onBatchComplete, signal, o
|
|
|
3082
3085
|
return results;
|
|
3083
3086
|
}
|
|
3084
3087
|
|
|
3088
|
+
// src/server/ai/pricing.ts
|
|
3089
|
+
function addUsage(into, add) {
|
|
3090
|
+
into.inputTokens += add.inputTokens;
|
|
3091
|
+
into.outputTokens += add.outputTokens;
|
|
3092
|
+
into.cacheCreationInputTokens += add.cacheCreationInputTokens;
|
|
3093
|
+
into.cacheReadInputTokens += add.cacheReadInputTokens;
|
|
3094
|
+
}
|
|
3095
|
+
var BATCH_PRICE_MULTIPLIER = 0.5;
|
|
3096
|
+
var CACHE_WRITE_MULTIPLIER = 1.25;
|
|
3097
|
+
var CACHE_READ_MULTIPLIER = 0.1;
|
|
3098
|
+
function usageCostUsd(usage, ai, multiplier = 1) {
|
|
3099
|
+
if (!usage) return void 0;
|
|
3100
|
+
const pricing = resolvePricing(ai);
|
|
3101
|
+
return pricing ? estimateUsageCostUsd(usage, pricing, multiplier) : void 0;
|
|
3102
|
+
}
|
|
3103
|
+
function estimateUsageCostUsd(usage, pricing, multiplier = 1) {
|
|
3104
|
+
const inputCost = (usage.inputTokens + usage.cacheCreationInputTokens * CACHE_WRITE_MULTIPLIER + usage.cacheReadInputTokens * CACHE_READ_MULTIPLIER) * pricing.inputPerMTok;
|
|
3105
|
+
return (inputCost + usage.outputTokens * pricing.outputPerMTok) / 1e6 * multiplier;
|
|
3106
|
+
}
|
|
3107
|
+
var PRICE_TABLE = [
|
|
3108
|
+
["claude-fable-5", 10, 50],
|
|
3109
|
+
["claude-mythos-5", 10, 50],
|
|
3110
|
+
// Deprecated Opus 4.1 / 4.0 cost 3x the 4.5+ generation — they must outrank
|
|
3111
|
+
// the shorter "claude-opus-4" prefix (4-2025 covers the dated Opus 4 full IDs).
|
|
3112
|
+
["claude-opus-4-1", 15, 75],
|
|
3113
|
+
["claude-opus-4-0", 15, 75],
|
|
3114
|
+
["claude-opus-4-2025", 15, 75],
|
|
3115
|
+
["claude-opus-4", 5, 25],
|
|
3116
|
+
["claude-sonnet-4", 3, 15],
|
|
3117
|
+
["claude-haiku-4", 1, 5],
|
|
3118
|
+
["claude-3-5-haiku", 0.8, 4],
|
|
3119
|
+
["gpt-5.5-pro", 30, 180],
|
|
3120
|
+
["gpt-5.5", 5, 30],
|
|
3121
|
+
["gpt-5.4-pro", 30, 180],
|
|
3122
|
+
["gpt-5.4-mini", 0.75, 4.5],
|
|
3123
|
+
["gpt-5.4-nano", 0.2, 1.25],
|
|
3124
|
+
["gpt-5.4", 2.5, 15],
|
|
3125
|
+
["gpt-5.3-codex", 1.75, 14]
|
|
3126
|
+
];
|
|
3127
|
+
var FREE_PROVIDERS = /* @__PURE__ */ new Set(["ollama", "claude-code"]);
|
|
3128
|
+
function bareModelId(model) {
|
|
3129
|
+
let id = model.trim().toLowerCase();
|
|
3130
|
+
const slash = id.lastIndexOf("/");
|
|
3131
|
+
if (slash !== -1) id = id.slice(slash + 1);
|
|
3132
|
+
const anth = id.lastIndexOf("anthropic.");
|
|
3133
|
+
if (anth !== -1) id = id.slice(anth + "anthropic.".length);
|
|
3134
|
+
return id;
|
|
3135
|
+
}
|
|
3136
|
+
function resolvePricing(ai) {
|
|
3137
|
+
if (ai.inputPricePerMTok !== void 0 && ai.outputPricePerMTok !== void 0) {
|
|
3138
|
+
return { source: "profile", inputPerMTok: ai.inputPricePerMTok, outputPerMTok: ai.outputPricePerMTok };
|
|
3139
|
+
}
|
|
3140
|
+
if (FREE_PROVIDERS.has(ai.provider)) return { source: "builtin", inputPerMTok: 0, outputPerMTok: 0 };
|
|
3141
|
+
const id = bareModelId(ai.model);
|
|
3142
|
+
let best;
|
|
3143
|
+
for (const row of PRICE_TABLE) {
|
|
3144
|
+
if (id.startsWith(row[0]) && (!best || row[0].length > best[0].length)) best = row;
|
|
3145
|
+
}
|
|
3146
|
+
return best ? { source: "builtin", inputPerMTok: best[1], outputPerMTok: best[2] } : null;
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3085
3149
|
// src/server/ai/anthropic.ts
|
|
3086
3150
|
var AnthropicProvider = class {
|
|
3087
3151
|
constructor(config, client) {
|
|
@@ -3097,9 +3161,25 @@ var AnthropicProvider = class {
|
|
|
3097
3161
|
}
|
|
3098
3162
|
config;
|
|
3099
3163
|
client;
|
|
3164
|
+
usage = { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
|
|
3100
3165
|
supportsVision() {
|
|
3101
3166
|
return true;
|
|
3102
3167
|
}
|
|
3168
|
+
recordUsage(usage) {
|
|
3169
|
+
if (!usage) return;
|
|
3170
|
+
addUsage(this.usage, {
|
|
3171
|
+
inputTokens: usage.input_tokens ?? 0,
|
|
3172
|
+
outputTokens: usage.output_tokens ?? 0,
|
|
3173
|
+
cacheCreationInputTokens: usage.cache_creation_input_tokens ?? 0,
|
|
3174
|
+
cacheReadInputTokens: usage.cache_read_input_tokens ?? 0
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
takeUsage() {
|
|
3178
|
+
const taken = this.usage;
|
|
3179
|
+
this.usage = { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
|
|
3180
|
+
const any = taken.inputTokens || taken.outputTokens || taken.cacheCreationInputTokens || taken.cacheReadInputTokens;
|
|
3181
|
+
return any ? taken : void 0;
|
|
3182
|
+
}
|
|
3103
3183
|
translate(reqs, onBatchComplete, signal, onMalformedReply) {
|
|
3104
3184
|
return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal, onMalformedReply);
|
|
3105
3185
|
}
|
|
@@ -3121,10 +3201,13 @@ var AnthropicProvider = class {
|
|
|
3121
3201
|
content.push({ type: "text", text: buildBatchPrompt(batch) });
|
|
3122
3202
|
return content;
|
|
3123
3203
|
}
|
|
3124
|
-
|
|
3125
|
-
|
|
3204
|
+
completionContent(req) {
|
|
3205
|
+
return req.content.map(
|
|
3126
3206
|
(b) => b.type === "image" ? { type: "image", source: { type: "base64", media_type: b.mediaType, data: b.base64 } } : { type: "text", text: b.text ?? "" }
|
|
3127
3207
|
);
|
|
3208
|
+
}
|
|
3209
|
+
async complete(req) {
|
|
3210
|
+
const content = this.completionContent(req);
|
|
3128
3211
|
const res = await this.client.messages.create({
|
|
3129
3212
|
model: this.config.model,
|
|
3130
3213
|
max_tokens: req.maxTokens ?? 8192,
|
|
@@ -3132,6 +3215,7 @@ var AnthropicProvider = class {
|
|
|
3132
3215
|
output_config: { format: { type: "json_schema", schema: req.schema } },
|
|
3133
3216
|
messages: [{ role: "user", content }]
|
|
3134
3217
|
});
|
|
3218
|
+
this.recordUsage(res.usage);
|
|
3135
3219
|
const text = res.content.find((b) => b.type === "text")?.text ?? "{}";
|
|
3136
3220
|
try {
|
|
3137
3221
|
return JSON.parse(text);
|
|
@@ -3173,6 +3257,7 @@ var AnthropicProvider = class {
|
|
|
3173
3257
|
out.set(entry.custom_id, { type: "failed", error: entry.result.error?.message ?? entry.result.type });
|
|
3174
3258
|
continue;
|
|
3175
3259
|
}
|
|
3260
|
+
this.recordUsage(entry.result.message?.usage);
|
|
3176
3261
|
const text = entry.result.message?.content.find((b) => b.type === "text")?.text ?? "";
|
|
3177
3262
|
try {
|
|
3178
3263
|
out.set(entry.custom_id, { type: "items", items: parseReplyItems(text) });
|
|
@@ -3186,6 +3271,40 @@ var AnthropicProvider = class {
|
|
|
3186
3271
|
async cancelTranslationBatch(batchId) {
|
|
3187
3272
|
await this.batchesClient().cancel(batchId);
|
|
3188
3273
|
}
|
|
3274
|
+
// Mirrors complete() exactly — same prompts and schema — so batch and sync
|
|
3275
|
+
// completion replies are interchangeable downstream.
|
|
3276
|
+
async submitCompletionBatch(jobs) {
|
|
3277
|
+
const requests = jobs.map((job) => ({
|
|
3278
|
+
custom_id: job.customId,
|
|
3279
|
+
params: {
|
|
3280
|
+
model: this.config.model,
|
|
3281
|
+
max_tokens: job.request.maxTokens ?? 8192,
|
|
3282
|
+
// Batch entries don't share a live cache window, so cache_control is omitted here.
|
|
3283
|
+
system: [{ type: "text", text: job.request.system }],
|
|
3284
|
+
output_config: { format: { type: "json_schema", schema: job.request.schema } },
|
|
3285
|
+
messages: [{ role: "user", content: this.completionContent(job.request) }]
|
|
3286
|
+
}
|
|
3287
|
+
}));
|
|
3288
|
+
const res = await this.batchesClient().create({ requests });
|
|
3289
|
+
return res.id;
|
|
3290
|
+
}
|
|
3291
|
+
async completionBatchResults(batchId) {
|
|
3292
|
+
const out = /* @__PURE__ */ new Map();
|
|
3293
|
+
for await (const entry of await this.batchesClient().results(batchId)) {
|
|
3294
|
+
if (entry.result.type !== "succeeded") {
|
|
3295
|
+
out.set(entry.custom_id, { type: "failed", error: entry.result.error?.message ?? entry.result.type });
|
|
3296
|
+
continue;
|
|
3297
|
+
}
|
|
3298
|
+
this.recordUsage(entry.result.message?.usage);
|
|
3299
|
+
const text = entry.result.message?.content.find((b) => b.type === "text")?.text ?? "";
|
|
3300
|
+
try {
|
|
3301
|
+
out.set(entry.custom_id, { type: "json", value: JSON.parse(text) });
|
|
3302
|
+
} catch {
|
|
3303
|
+
out.set(entry.custom_id, { type: "malformed", raw: text });
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
return out;
|
|
3307
|
+
}
|
|
3189
3308
|
async callBatch(batch, signal) {
|
|
3190
3309
|
const content = this.buildUserContent(batch);
|
|
3191
3310
|
const res = await this.client.messages.create({
|
|
@@ -3195,6 +3314,7 @@ var AnthropicProvider = class {
|
|
|
3195
3314
|
output_config: { format: { type: "json_schema", schema: BATCH_SCHEMA } },
|
|
3196
3315
|
messages: [{ role: "user", content }]
|
|
3197
3316
|
}, { signal });
|
|
3317
|
+
this.recordUsage(res.usage);
|
|
3198
3318
|
const text = res.content.find((b) => b.type === "text")?.text ?? "";
|
|
3199
3319
|
return parseReplyItems(text);
|
|
3200
3320
|
}
|
|
@@ -3760,6 +3880,24 @@ function clearPendingBatch(projectRoot) {
|
|
|
3760
3880
|
rmSync4(pendingBatchPath(projectRoot), { force: true });
|
|
3761
3881
|
}
|
|
3762
3882
|
|
|
3883
|
+
// src/server/log.ts
|
|
3884
|
+
import { appendFileSync, readFileSync as readFileSync9, existsSync as existsSync9 } from "fs";
|
|
3885
|
+
import { resolve as resolve6 } from "path";
|
|
3886
|
+
function logPath(projectRoot) {
|
|
3887
|
+
return resolve6(projectRoot, ".glotfile", "log.jsonl");
|
|
3888
|
+
}
|
|
3889
|
+
function appendLog(projectRoot, entry) {
|
|
3890
|
+
ensureGlotfileDir(projectRoot);
|
|
3891
|
+
appendFileSync(logPath(projectRoot), JSON.stringify(entry) + "\n", "utf8");
|
|
3892
|
+
}
|
|
3893
|
+
function readLog(projectRoot, limit = 100) {
|
|
3894
|
+
const path = logPath(projectRoot);
|
|
3895
|
+
if (!existsSync9(path)) return [];
|
|
3896
|
+
const lines = readFileSync9(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
3897
|
+
const entries = lines.map((l) => JSON.parse(l));
|
|
3898
|
+
return entries.reverse().slice(0, limit);
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3763
3901
|
// src/server/ai/batch-run.ts
|
|
3764
3902
|
function buildBatchJobs(reqs, batchSize) {
|
|
3765
3903
|
const byLocale = /* @__PURE__ */ new Map();
|
|
@@ -3807,7 +3945,9 @@ async function submitBatchTranslation(state, provider, reqs, batchSize, model, p
|
|
|
3807
3945
|
return pending;
|
|
3808
3946
|
}
|
|
3809
3947
|
async function applyBatchResults(load, persist, provider, pending, projectRoot, ai) {
|
|
3948
|
+
provider.takeUsage?.();
|
|
3810
3949
|
const outcomes = await provider.translationBatchResults(pending.batchId);
|
|
3950
|
+
const batchUsage = provider.takeUsage?.();
|
|
3811
3951
|
const fresh = load();
|
|
3812
3952
|
const isStale = (r) => {
|
|
3813
3953
|
const entry = fresh.keys[r.key];
|
|
@@ -3816,13 +3956,19 @@ async function applyBatchResults(load, persist, provider, pending, projectRoot,
|
|
|
3816
3956
|
const applied = [];
|
|
3817
3957
|
const results = [];
|
|
3818
3958
|
const retryReqs = [];
|
|
3819
|
-
|
|
3959
|
+
const stale = [];
|
|
3960
|
+
const jobFailures = [];
|
|
3820
3961
|
for (const job of pending.jobs) {
|
|
3821
3962
|
const outcome = outcomes.get(job.customId);
|
|
3822
3963
|
const itemsById = outcome?.type === "items" ? new Map(outcome.items.map((i) => [i.id, i])) : null;
|
|
3964
|
+
if (!itemsById) {
|
|
3965
|
+
if (!outcome) jobFailures.push({ customId: job.customId, locale: job.locale, type: "missing" });
|
|
3966
|
+
else if (outcome.type === "malformed") jobFailures.push({ customId: job.customId, locale: job.locale, type: "malformed", raw: outcome.raw });
|
|
3967
|
+
else if (outcome.type === "failed") jobFailures.push({ customId: job.customId, locale: job.locale, type: "failed", error: outcome.error });
|
|
3968
|
+
}
|
|
3823
3969
|
for (const stored of job.requests) {
|
|
3824
3970
|
if (isStale(stored)) {
|
|
3825
|
-
|
|
3971
|
+
stale.push({ key: stored.key, locale: stored.targetLocale });
|
|
3826
3972
|
continue;
|
|
3827
3973
|
}
|
|
3828
3974
|
const { sourceHash: _hash, ...req } = stored;
|
|
@@ -3841,7 +3987,20 @@ async function applyBatchResults(load, persist, provider, pending, projectRoot,
|
|
|
3841
3987
|
const retryResults = await runLocaleParallel(
|
|
3842
3988
|
retryReqs,
|
|
3843
3989
|
provider,
|
|
3844
|
-
{
|
|
3990
|
+
{
|
|
3991
|
+
// Record the raw reply so an unparseable retry response is diagnosable
|
|
3992
|
+
// from the activity log instead of vanishing into per-item errors.
|
|
3993
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
3994
|
+
appendLog(projectRoot, {
|
|
3995
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3996
|
+
kind: "translate",
|
|
3997
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
3998
|
+
model: pending.model,
|
|
3999
|
+
locale,
|
|
4000
|
+
raw
|
|
4001
|
+
});
|
|
4002
|
+
}
|
|
4003
|
+
},
|
|
3845
4004
|
ai.concurrency,
|
|
3846
4005
|
void 0,
|
|
3847
4006
|
ai.batchSize
|
|
@@ -3849,53 +4008,161 @@ async function applyBatchResults(load, persist, provider, pending, projectRoot,
|
|
|
3849
4008
|
applied.push(...retryReqs);
|
|
3850
4009
|
results.push(...retryResults);
|
|
3851
4010
|
}
|
|
4011
|
+
const retryUsage = provider.takeUsage?.();
|
|
4012
|
+
const pricing = resolvePricing({ ...ai, model: pending.model });
|
|
4013
|
+
let estimatedCostUsd;
|
|
4014
|
+
if (pricing && (batchUsage || retryUsage)) {
|
|
4015
|
+
estimatedCostUsd = (batchUsage ? estimateUsageCostUsd(batchUsage, pricing, BATCH_PRICE_MULTIPLIER) : 0) + (retryUsage ? estimateUsageCostUsd(retryUsage, pricing) : 0);
|
|
4016
|
+
}
|
|
4017
|
+
let usage;
|
|
4018
|
+
if (batchUsage || retryUsage) {
|
|
4019
|
+
usage = batchUsage ?? { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
|
|
4020
|
+
if (retryUsage) addUsage(usage, retryUsage);
|
|
4021
|
+
}
|
|
3852
4022
|
const { written, errors } = applyResults(fresh, applied, results);
|
|
3853
4023
|
persist(fresh);
|
|
3854
4024
|
clearPendingBatch(projectRoot);
|
|
3855
|
-
|
|
4025
|
+
const costSuffix = estimatedCostUsd !== void 0 ? ` (~$${estimatedCostUsd.toFixed(2)})` : "";
|
|
4026
|
+
appendLog(projectRoot, {
|
|
4027
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4028
|
+
kind: "translate",
|
|
4029
|
+
summary: `Applied batch ${pending.batchId}: wrote ${written}, ${errors.length} error(s), ${retryReqs.length} retried, ${stale.length} stale${costSuffix}`,
|
|
4030
|
+
model: pending.model,
|
|
4031
|
+
items: applied.map((r) => ({ id: r.id, key: r.key, source: r.source, targetLocale: r.targetLocale })),
|
|
4032
|
+
results,
|
|
4033
|
+
jobFailures: jobFailures.length ? jobFailures : void 0,
|
|
4034
|
+
stale: stale.length ? stale : void 0,
|
|
4035
|
+
usage,
|
|
4036
|
+
estimatedCostUsd
|
|
4037
|
+
});
|
|
4038
|
+
return { written, errors, staleSkipped: stale.length, retried: retryReqs.length, screenshotsSkipped };
|
|
3856
4039
|
}
|
|
3857
4040
|
|
|
3858
|
-
// src/server/ai/
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
// the shorter "claude-opus-4" prefix (4-2025 covers the dated Opus 4 full IDs).
|
|
3864
|
-
["claude-opus-4-1", 15, 75],
|
|
3865
|
-
["claude-opus-4-0", 15, 75],
|
|
3866
|
-
["claude-opus-4-2025", 15, 75],
|
|
3867
|
-
["claude-opus-4", 5, 25],
|
|
3868
|
-
["claude-sonnet-4", 3, 15],
|
|
3869
|
-
["claude-haiku-4", 1, 5],
|
|
3870
|
-
["claude-3-5-haiku", 0.8, 4],
|
|
3871
|
-
["gpt-5.5-pro", 30, 180],
|
|
3872
|
-
["gpt-5.5", 5, 30],
|
|
3873
|
-
["gpt-5.4-pro", 30, 180],
|
|
3874
|
-
["gpt-5.4-mini", 0.75, 4.5],
|
|
3875
|
-
["gpt-5.4-nano", 0.2, 1.25],
|
|
3876
|
-
["gpt-5.4", 2.5, 15],
|
|
3877
|
-
["gpt-5.3-codex", 1.75, 14]
|
|
3878
|
-
];
|
|
3879
|
-
var FREE_PROVIDERS = /* @__PURE__ */ new Set(["ollama", "claude-code"]);
|
|
3880
|
-
function bareModelId(model) {
|
|
3881
|
-
let id = model.trim().toLowerCase();
|
|
3882
|
-
const slash = id.lastIndexOf("/");
|
|
3883
|
-
if (slash !== -1) id = id.slice(slash + 1);
|
|
3884
|
-
const anth = id.lastIndexOf("anthropic.");
|
|
3885
|
-
if (anth !== -1) id = id.slice(anth + "anthropic.".length);
|
|
3886
|
-
return id;
|
|
4041
|
+
// src/server/ai/pending-context-batch.ts
|
|
4042
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync4, rmSync as rmSync5 } from "fs";
|
|
4043
|
+
import { join as join5 } from "path";
|
|
4044
|
+
function pendingContextBatchPath(projectRoot) {
|
|
4045
|
+
return join5(projectRoot, ".glotfile", "context-batch.json");
|
|
3887
4046
|
}
|
|
3888
|
-
function
|
|
3889
|
-
|
|
3890
|
-
|
|
4047
|
+
function loadPendingContextBatch(projectRoot) {
|
|
4048
|
+
const path = pendingContextBatchPath(projectRoot);
|
|
4049
|
+
if (!existsSync10(path)) return void 0;
|
|
4050
|
+
try {
|
|
4051
|
+
const parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
4052
|
+
if (parsed?.version !== 1) return void 0;
|
|
4053
|
+
return parsed;
|
|
4054
|
+
} catch {
|
|
4055
|
+
return void 0;
|
|
3891
4056
|
}
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
4057
|
+
}
|
|
4058
|
+
function savePendingContextBatch(projectRoot, pending) {
|
|
4059
|
+
const dir = join5(projectRoot, ".glotfile");
|
|
4060
|
+
mkdirSync5(dir, { recursive: true });
|
|
4061
|
+
const gitignore = join5(dir, ".gitignore");
|
|
4062
|
+
if (!existsSync10(gitignore)) writeFileSync4(gitignore, "*\n");
|
|
4063
|
+
writeFileSync4(pendingContextBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
|
|
4064
|
+
}
|
|
4065
|
+
function clearPendingContextBatch(projectRoot) {
|
|
4066
|
+
rmSync5(pendingContextBatchPath(projectRoot), { force: true });
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
// src/server/ai/context-batch-run.ts
|
|
4070
|
+
function completionRequestFor(chunk2) {
|
|
4071
|
+
return {
|
|
4072
|
+
system: buildContextSystemPrompt(),
|
|
4073
|
+
content: [{ type: "text", text: buildContextBatchPrompt(chunk2) }],
|
|
4074
|
+
schema: CONTEXT_BATCH_SCHEMA
|
|
4075
|
+
};
|
|
4076
|
+
}
|
|
4077
|
+
async function submitContextBatch(provider, targets, batchSize, model, projectRoot, force) {
|
|
4078
|
+
if (loadPendingContextBatch(projectRoot)) {
|
|
4079
|
+
throw new Error("A context batch is already pending. Apply or cancel it first.");
|
|
3897
4080
|
}
|
|
3898
|
-
|
|
4081
|
+
const chunks = [];
|
|
4082
|
+
const size = Math.max(1, batchSize);
|
|
4083
|
+
for (let i = 0; i < targets.length; i += size) chunks.push(targets.slice(i, i + size));
|
|
4084
|
+
const jobs = chunks.map((chunk2, i) => ({ customId: `ctx_${i}`, chunk: chunk2 }));
|
|
4085
|
+
const batchId = await provider.submitCompletionBatch(
|
|
4086
|
+
jobs.map((j) => ({ customId: j.customId, request: completionRequestFor(j.chunk) }))
|
|
4087
|
+
);
|
|
4088
|
+
const pending = {
|
|
4089
|
+
version: 1,
|
|
4090
|
+
// Only Anthropic implements completion batches today.
|
|
4091
|
+
provider: "anthropic",
|
|
4092
|
+
model,
|
|
4093
|
+
batchId,
|
|
4094
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4095
|
+
total: targets.length,
|
|
4096
|
+
force,
|
|
4097
|
+
jobs: jobs.map((j) => ({
|
|
4098
|
+
customId: j.customId,
|
|
4099
|
+
requests: j.chunk.map(({ image: _image, ...rest }) => rest)
|
|
4100
|
+
}))
|
|
4101
|
+
};
|
|
4102
|
+
savePendingContextBatch(projectRoot, pending);
|
|
4103
|
+
return pending;
|
|
4104
|
+
}
|
|
4105
|
+
async function applyContextBatchResults(load, persist, provider, pending, projectRoot, ai) {
|
|
4106
|
+
provider.takeUsage?.();
|
|
4107
|
+
const outcomes = await provider.completionBatchResults(pending.batchId);
|
|
4108
|
+
const batchUsage = provider.takeUsage?.();
|
|
4109
|
+
const applied = [];
|
|
4110
|
+
const items = [];
|
|
4111
|
+
const errors = [];
|
|
4112
|
+
const jobFailures = [];
|
|
4113
|
+
const retryChunks = [];
|
|
4114
|
+
for (const job of pending.jobs) {
|
|
4115
|
+
const outcome = outcomes.get(job.customId);
|
|
4116
|
+
if (outcome?.type === "json") {
|
|
4117
|
+
const batch = outcome.value;
|
|
4118
|
+
applied.push(...job.requests);
|
|
4119
|
+
items.push(...batch.items ?? []);
|
|
4120
|
+
continue;
|
|
4121
|
+
}
|
|
4122
|
+
if (!outcome) jobFailures.push({ customId: job.customId, locale: "", type: "missing" });
|
|
4123
|
+
else if (outcome.type === "malformed") jobFailures.push({ customId: job.customId, locale: "", type: "malformed", raw: outcome.raw });
|
|
4124
|
+
else jobFailures.push({ customId: job.customId, locale: "", type: "failed", error: outcome.error });
|
|
4125
|
+
retryChunks.push(job.requests);
|
|
4126
|
+
}
|
|
4127
|
+
for (const chunk2 of retryChunks) {
|
|
4128
|
+
try {
|
|
4129
|
+
const raw = await provider.complete(completionRequestFor(chunk2));
|
|
4130
|
+
const batch = raw;
|
|
4131
|
+
applied.push(...chunk2);
|
|
4132
|
+
items.push(...batch.items ?? []);
|
|
4133
|
+
} catch (e) {
|
|
4134
|
+
errors.push(...chunk2.map((t) => ({ key: t.key, error: e.message })));
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
const retryUsage = provider.takeUsage?.();
|
|
4138
|
+
const pricing = resolvePricing({ ...ai, model: pending.model });
|
|
4139
|
+
let estimatedCostUsd;
|
|
4140
|
+
if (pricing && (batchUsage || retryUsage)) {
|
|
4141
|
+
estimatedCostUsd = (batchUsage ? estimateUsageCostUsd(batchUsage, pricing, BATCH_PRICE_MULTIPLIER) : 0) + (retryUsage ? estimateUsageCostUsd(retryUsage, pricing) : 0);
|
|
4142
|
+
}
|
|
4143
|
+
let usage;
|
|
4144
|
+
if (batchUsage || retryUsage) {
|
|
4145
|
+
usage = batchUsage ?? { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
|
|
4146
|
+
if (retryUsage) addUsage(usage, retryUsage);
|
|
4147
|
+
}
|
|
4148
|
+
const fresh = load();
|
|
4149
|
+
const { written, errors: applyErrors } = applyContext(fresh, applied, items, void 0, pending.force);
|
|
4150
|
+
errors.push(...applyErrors);
|
|
4151
|
+
persist(fresh);
|
|
4152
|
+
clearPendingContextBatch(projectRoot);
|
|
4153
|
+
const costSuffix = estimatedCostUsd !== void 0 ? ` (~$${estimatedCostUsd.toFixed(2)})` : "";
|
|
4154
|
+
appendLog(projectRoot, {
|
|
4155
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4156
|
+
kind: "context",
|
|
4157
|
+
summary: `Applied context batch ${pending.batchId}: wrote ${written}, ${errors.length} error(s), ${retryChunks.length} job(s) retried${costSuffix}`,
|
|
4158
|
+
model: pending.model,
|
|
4159
|
+
items: applied.map((r) => ({ id: r.id, key: r.key, source: r.source })),
|
|
4160
|
+
results: items.map((r) => ({ id: r.id, value: r.context, error: r.error })),
|
|
4161
|
+
jobFailures: jobFailures.length ? jobFailures : void 0,
|
|
4162
|
+
usage,
|
|
4163
|
+
estimatedCostUsd
|
|
4164
|
+
});
|
|
4165
|
+
return { written, errors, retried: retryChunks.length };
|
|
3899
4166
|
}
|
|
3900
4167
|
|
|
3901
4168
|
// src/server/ai/estimate.ts
|
|
@@ -3951,30 +4218,12 @@ function estimateTranslation(state, ai, opts) {
|
|
|
3951
4218
|
};
|
|
3952
4219
|
}
|
|
3953
4220
|
|
|
3954
|
-
// src/server/log.ts
|
|
3955
|
-
import { appendFileSync, readFileSync as readFileSync9, existsSync as existsSync9 } from "fs";
|
|
3956
|
-
import { resolve as resolve6 } from "path";
|
|
3957
|
-
function logPath(projectRoot) {
|
|
3958
|
-
return resolve6(projectRoot, ".glotfile", "log.jsonl");
|
|
3959
|
-
}
|
|
3960
|
-
function appendLog(projectRoot, entry) {
|
|
3961
|
-
ensureGlotfileDir(projectRoot);
|
|
3962
|
-
appendFileSync(logPath(projectRoot), JSON.stringify(entry) + "\n", "utf8");
|
|
3963
|
-
}
|
|
3964
|
-
function readLog(projectRoot, limit = 100) {
|
|
3965
|
-
const path = logPath(projectRoot);
|
|
3966
|
-
if (!existsSync9(path)) return [];
|
|
3967
|
-
const lines = readFileSync9(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
3968
|
-
const entries = lines.map((l) => JSON.parse(l));
|
|
3969
|
-
return entries.reverse().slice(0, limit);
|
|
3970
|
-
}
|
|
3971
|
-
|
|
3972
4221
|
// src/server/import/run.ts
|
|
3973
4222
|
import { relative as relative3 } from "path";
|
|
3974
4223
|
|
|
3975
4224
|
// src/server/import/detect.ts
|
|
3976
|
-
import { existsSync as
|
|
3977
|
-
import { join as
|
|
4225
|
+
import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync11, statSync as statSync2 } from "fs";
|
|
4226
|
+
import { join as join6 } from "path";
|
|
3978
4227
|
var LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
3979
4228
|
var VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
|
|
3980
4229
|
function safeIsDir(p) {
|
|
@@ -3985,7 +4234,7 @@ function safeIsDir(p) {
|
|
|
3985
4234
|
}
|
|
3986
4235
|
}
|
|
3987
4236
|
function listDirs(dir) {
|
|
3988
|
-
return readdirSync3(dir).filter((e) => safeIsDir(
|
|
4237
|
+
return readdirSync3(dir).filter((e) => safeIsDir(join6(dir, e)));
|
|
3989
4238
|
}
|
|
3990
4239
|
function fileCount(dir) {
|
|
3991
4240
|
try {
|
|
@@ -3999,23 +4248,23 @@ function pickSource(locales, sizeOf) {
|
|
|
3999
4248
|
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
4000
4249
|
}
|
|
4001
4250
|
function detectLaravel(root) {
|
|
4002
|
-
const localeRoot = [
|
|
4251
|
+
const localeRoot = [join6(root, "resources", "lang"), join6(root, "lang")].find(safeIsDir);
|
|
4003
4252
|
if (!localeRoot) return null;
|
|
4004
4253
|
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
4005
4254
|
if (locales.length === 0) return null;
|
|
4006
|
-
const sourceLocale = pickSource(locales, (loc) => fileCount(
|
|
4255
|
+
const sourceLocale = pickSource(locales, (loc) => fileCount(join6(localeRoot, loc)));
|
|
4007
4256
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
4008
4257
|
}
|
|
4009
4258
|
function detectVue(root, forced = false) {
|
|
4010
4259
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
4011
|
-
const localeRoot =
|
|
4260
|
+
const localeRoot = join6(root, rel);
|
|
4012
4261
|
if (!safeIsDir(localeRoot)) continue;
|
|
4013
4262
|
const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4014
4263
|
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
4015
4264
|
if (enough) {
|
|
4016
4265
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4017
4266
|
try {
|
|
4018
|
-
return statSync2(
|
|
4267
|
+
return statSync2(join6(localeRoot, `${loc}.json`)).size;
|
|
4019
4268
|
} catch {
|
|
4020
4269
|
return 0;
|
|
4021
4270
|
}
|
|
@@ -4027,7 +4276,7 @@ function detectVue(root, forced = false) {
|
|
|
4027
4276
|
}
|
|
4028
4277
|
function detectArb(root) {
|
|
4029
4278
|
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
4030
|
-
const localeRoot =
|
|
4279
|
+
const localeRoot = join6(root, rel);
|
|
4031
4280
|
if (!safeIsDir(localeRoot)) continue;
|
|
4032
4281
|
const locales = readdirSync3(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
4033
4282
|
if (locales.length >= 1) {
|
|
@@ -4037,10 +4286,10 @@ function detectArb(root) {
|
|
|
4037
4286
|
return null;
|
|
4038
4287
|
}
|
|
4039
4288
|
function lprojLocales(dir) {
|
|
4040
|
-
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) &&
|
|
4289
|
+
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join6(dir, `${l}.lproj`, "Localizable.strings")));
|
|
4041
4290
|
}
|
|
4042
4291
|
function detectApple(root) {
|
|
4043
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
4292
|
+
const candidates = [root, ...listDirs(root).map((d) => join6(root, d))];
|
|
4044
4293
|
let best = null;
|
|
4045
4294
|
for (const dir of candidates) {
|
|
4046
4295
|
const locales = lprojLocales(dir);
|
|
@@ -4052,7 +4301,7 @@ function detectApple(root) {
|
|
|
4052
4301
|
locales,
|
|
4053
4302
|
sourceLocale: pickSource(locales, (loc) => {
|
|
4054
4303
|
try {
|
|
4055
|
-
return statSync2(
|
|
4304
|
+
return statSync2(join6(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
4056
4305
|
} catch {
|
|
4057
4306
|
return 0;
|
|
4058
4307
|
}
|
|
@@ -4065,7 +4314,7 @@ function detectApple(root) {
|
|
|
4065
4314
|
var ANGULAR_DIR_CANDIDATES = [".", "src/locale", "src/locales", "src/i18n", "locale", "locales", "i18n", "translations"];
|
|
4066
4315
|
function detectAngularXliff(root) {
|
|
4067
4316
|
for (const rel of ANGULAR_DIR_CANDIDATES) {
|
|
4068
|
-
const localeRoot = rel === "." ? root :
|
|
4317
|
+
const localeRoot = rel === "." ? root : join6(root, rel);
|
|
4069
4318
|
if (!safeIsDir(localeRoot)) continue;
|
|
4070
4319
|
const files = readdirSync3(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
|
|
4071
4320
|
if (files.length === 0) continue;
|
|
@@ -4073,7 +4322,7 @@ function detectAngularXliff(root) {
|
|
|
4073
4322
|
const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
|
|
4074
4323
|
let sourceLocale;
|
|
4075
4324
|
try {
|
|
4076
|
-
sourceLocale =
|
|
4325
|
+
sourceLocale = readFileSync11(join6(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
|
|
4077
4326
|
} catch {
|
|
4078
4327
|
}
|
|
4079
4328
|
if (!sourceLocale && locales.length === 0) continue;
|
|
@@ -4084,14 +4333,14 @@ function detectAngularXliff(root) {
|
|
|
4084
4333
|
return null;
|
|
4085
4334
|
}
|
|
4086
4335
|
function detectRails(root) {
|
|
4087
|
-
const localeRoot =
|
|
4336
|
+
const localeRoot = join6(root, "config", "locales");
|
|
4088
4337
|
if (!safeIsDir(localeRoot)) return null;
|
|
4089
4338
|
const locales = [];
|
|
4090
4339
|
for (const file of readdirSync3(localeRoot).sort()) {
|
|
4091
4340
|
if (!/\.ya?ml$/.test(file)) continue;
|
|
4092
4341
|
let text;
|
|
4093
4342
|
try {
|
|
4094
|
-
text =
|
|
4343
|
+
text = readFileSync11(join6(localeRoot, file), "utf8");
|
|
4095
4344
|
} catch {
|
|
4096
4345
|
continue;
|
|
4097
4346
|
}
|
|
@@ -4106,15 +4355,15 @@ function detectRails(root) {
|
|
|
4106
4355
|
var I18NEXT_DIR_CANDIDATES = ["public/locales", "static/locales", "locales", "src/locales", "src/i18n/locales"];
|
|
4107
4356
|
function detectI18next(root) {
|
|
4108
4357
|
for (const rel of I18NEXT_DIR_CANDIDATES) {
|
|
4109
|
-
const localeRoot =
|
|
4358
|
+
const localeRoot = join6(root, rel);
|
|
4110
4359
|
if (!safeIsDir(localeRoot)) continue;
|
|
4111
4360
|
const locales = listDirs(localeRoot).filter(
|
|
4112
|
-
(d) => LOCALE_RE.test(d) && readdirSync3(
|
|
4361
|
+
(d) => LOCALE_RE.test(d) && readdirSync3(join6(localeRoot, d)).some((f) => f.endsWith(".json"))
|
|
4113
4362
|
);
|
|
4114
4363
|
if (locales.length === 0) continue;
|
|
4115
4364
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4116
4365
|
try {
|
|
4117
|
-
return readdirSync3(
|
|
4366
|
+
return readdirSync3(join6(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync2(join6(localeRoot, loc, f)).size, 0);
|
|
4118
4367
|
} catch {
|
|
4119
4368
|
return 0;
|
|
4120
4369
|
}
|
|
@@ -4131,8 +4380,8 @@ function gettextLocales(dir) {
|
|
|
4131
4380
|
if (!locales.includes(flat)) locales.push(flat);
|
|
4132
4381
|
continue;
|
|
4133
4382
|
}
|
|
4134
|
-
if (!LOCALE_RE.test(entry) || !safeIsDir(
|
|
4135
|
-
const sub =
|
|
4383
|
+
if (!LOCALE_RE.test(entry) || !safeIsDir(join6(dir, entry))) continue;
|
|
4384
|
+
const sub = join6(dir, entry);
|
|
4136
4385
|
const hasPo = (d) => {
|
|
4137
4386
|
try {
|
|
4138
4387
|
return readdirSync3(d).some((f) => f.endsWith(".po"));
|
|
@@ -4140,7 +4389,7 @@ function gettextLocales(dir) {
|
|
|
4140
4389
|
return false;
|
|
4141
4390
|
}
|
|
4142
4391
|
};
|
|
4143
|
-
if (hasPo(
|
|
4392
|
+
if (hasPo(join6(sub, "LC_MESSAGES")) || hasPo(sub)) {
|
|
4144
4393
|
if (!locales.includes(entry)) locales.push(entry);
|
|
4145
4394
|
}
|
|
4146
4395
|
}
|
|
@@ -4149,7 +4398,7 @@ function gettextLocales(dir) {
|
|
|
4149
4398
|
var GETTEXT_DIR_CANDIDATES = ["locale", "locales", "po", "translations"];
|
|
4150
4399
|
function detectGettext(root) {
|
|
4151
4400
|
for (const rel of GETTEXT_DIR_CANDIDATES) {
|
|
4152
|
-
const localeRoot =
|
|
4401
|
+
const localeRoot = join6(root, rel);
|
|
4153
4402
|
if (!safeIsDir(localeRoot)) continue;
|
|
4154
4403
|
const locales = gettextLocales(localeRoot);
|
|
4155
4404
|
if (locales.length === 0) continue;
|
|
@@ -4158,10 +4407,10 @@ function detectGettext(root) {
|
|
|
4158
4407
|
return null;
|
|
4159
4408
|
}
|
|
4160
4409
|
function detectAppleStringsdict(root) {
|
|
4161
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
4410
|
+
const candidates = [root, ...listDirs(root).map((d) => join6(root, d))];
|
|
4162
4411
|
let best = null;
|
|
4163
4412
|
for (const dir of candidates) {
|
|
4164
|
-
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) &&
|
|
4413
|
+
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join6(dir, `${l}.lproj`, "Localizable.stringsdict")));
|
|
4165
4414
|
if (locales.length === 0) continue;
|
|
4166
4415
|
if (!best || locales.length > best.locales.length) {
|
|
4167
4416
|
best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
@@ -4192,7 +4441,7 @@ var BY_FORMAT = {
|
|
|
4192
4441
|
"apple-stringsdict": detectAppleStringsdict
|
|
4193
4442
|
};
|
|
4194
4443
|
function detect(root, formatOverride) {
|
|
4195
|
-
if (!
|
|
4444
|
+
if (!existsSync11(root)) return null;
|
|
4196
4445
|
if (formatOverride) {
|
|
4197
4446
|
const fn = BY_FORMAT[formatOverride];
|
|
4198
4447
|
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
@@ -4206,8 +4455,8 @@ function detect(root, formatOverride) {
|
|
|
4206
4455
|
}
|
|
4207
4456
|
|
|
4208
4457
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
4209
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
4210
|
-
import { join as
|
|
4458
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync12 } from "fs";
|
|
4459
|
+
import { join as join7 } from "path";
|
|
4211
4460
|
|
|
4212
4461
|
// src/server/import/flatten.ts
|
|
4213
4462
|
function flattenObject(value, prefix, warnings) {
|
|
@@ -4246,7 +4495,7 @@ var vueI18nJson2 = {
|
|
|
4246
4495
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4247
4496
|
let data;
|
|
4248
4497
|
try {
|
|
4249
|
-
data = JSON.parse(
|
|
4498
|
+
data = JSON.parse(readFileSync12(join7(localeRoot, file), "utf8"));
|
|
4250
4499
|
} catch (e) {
|
|
4251
4500
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
4252
4501
|
continue;
|
|
@@ -4262,7 +4511,7 @@ var vueI18nJson2 = {
|
|
|
4262
4511
|
|
|
4263
4512
|
// src/server/import/parsers/laravel-php.ts
|
|
4264
4513
|
import { readdirSync as readdirSync5, statSync as statSync3 } from "fs";
|
|
4265
|
-
import { join as
|
|
4514
|
+
import { join as join8, relative as relative2 } from "path";
|
|
4266
4515
|
import { execFileSync } from "child_process";
|
|
4267
4516
|
|
|
4268
4517
|
// src/server/import/placeholders.ts
|
|
@@ -4272,13 +4521,13 @@ function laravelToCanonical(value) {
|
|
|
4272
4521
|
|
|
4273
4522
|
// src/server/import/parsers/laravel-php.ts
|
|
4274
4523
|
function listDirs2(dir) {
|
|
4275
|
-
return readdirSync5(dir).filter((e) => statSync3(
|
|
4524
|
+
return readdirSync5(dir).filter((e) => statSync3(join8(dir, e)).isDirectory());
|
|
4276
4525
|
}
|
|
4277
4526
|
function listPhpFiles(dir) {
|
|
4278
4527
|
const out = [];
|
|
4279
4528
|
const walk = (d) => {
|
|
4280
4529
|
for (const e of readdirSync5(d)) {
|
|
4281
|
-
const full =
|
|
4530
|
+
const full = join8(d, e);
|
|
4282
4531
|
if (statSync3(full).isDirectory()) walk(full);
|
|
4283
4532
|
else if (e.endsWith(".php")) out.push(full);
|
|
4284
4533
|
}
|
|
@@ -4315,7 +4564,7 @@ var laravelPhp2 = {
|
|
|
4315
4564
|
for (const locale of listDirs2(localeRoot).sort()) {
|
|
4316
4565
|
if (locale === "vendor") continue;
|
|
4317
4566
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4318
|
-
const localeDir =
|
|
4567
|
+
const localeDir = join8(localeRoot, locale);
|
|
4319
4568
|
locales.push(locale);
|
|
4320
4569
|
for (const file of listPhpFiles(localeDir)) {
|
|
4321
4570
|
const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
|
|
@@ -4338,8 +4587,8 @@ var laravelPhp2 = {
|
|
|
4338
4587
|
};
|
|
4339
4588
|
|
|
4340
4589
|
// src/server/import/parsers/flutter-arb.ts
|
|
4341
|
-
import { readdirSync as readdirSync6, readFileSync as
|
|
4342
|
-
import { join as
|
|
4590
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync13 } from "fs";
|
|
4591
|
+
import { join as join9 } from "path";
|
|
4343
4592
|
var LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4344
4593
|
function localeFromArbName(file) {
|
|
4345
4594
|
const m = file.match(/^(.+)\.arb$/);
|
|
@@ -4375,7 +4624,7 @@ var flutterArb2 = {
|
|
|
4375
4624
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4376
4625
|
let data;
|
|
4377
4626
|
try {
|
|
4378
|
-
data = JSON.parse(
|
|
4627
|
+
data = JSON.parse(readFileSync13(join9(localeRoot, file), "utf8"));
|
|
4379
4628
|
} catch (e) {
|
|
4380
4629
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
4381
4630
|
continue;
|
|
@@ -4400,8 +4649,8 @@ var flutterArb2 = {
|
|
|
4400
4649
|
};
|
|
4401
4650
|
|
|
4402
4651
|
// src/server/import/parsers/apple-strings.ts
|
|
4403
|
-
import { readdirSync as readdirSync7, readFileSync as
|
|
4404
|
-
import { join as
|
|
4652
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync14, statSync as statSync4 } from "fs";
|
|
4653
|
+
import { join as join10 } from "path";
|
|
4405
4654
|
var LOCALE_RE4 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4406
4655
|
var TABLE = "Localizable.strings";
|
|
4407
4656
|
function localeFromLproj(dir) {
|
|
@@ -4505,16 +4754,16 @@ var appleStrings2 = {
|
|
|
4505
4754
|
const locale = localeFromLproj(dir);
|
|
4506
4755
|
if (!locale) continue;
|
|
4507
4756
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4508
|
-
const file =
|
|
4757
|
+
const file = join10(localeRoot, dir, TABLE);
|
|
4509
4758
|
let text;
|
|
4510
4759
|
try {
|
|
4511
4760
|
if (!statSync4(file).isFile()) continue;
|
|
4512
|
-
text =
|
|
4761
|
+
text = readFileSync14(file, "utf8");
|
|
4513
4762
|
} catch {
|
|
4514
4763
|
continue;
|
|
4515
4764
|
}
|
|
4516
4765
|
locales.push(locale);
|
|
4517
|
-
const others = readdirSync7(
|
|
4766
|
+
const others = readdirSync7(join10(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
|
|
4518
4767
|
if (others.length) {
|
|
4519
4768
|
warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
|
|
4520
4769
|
}
|
|
@@ -4527,8 +4776,8 @@ var appleStrings2 = {
|
|
|
4527
4776
|
};
|
|
4528
4777
|
|
|
4529
4778
|
// src/server/import/parsers/angular-xliff.ts
|
|
4530
|
-
import { readdirSync as readdirSync8, readFileSync as
|
|
4531
|
-
import { join as
|
|
4779
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync15 } from "fs";
|
|
4780
|
+
import { join as join11 } from "path";
|
|
4532
4781
|
var LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4533
4782
|
var FILE_RE = /^messages(?:\.(.+))?\.xlf$/;
|
|
4534
4783
|
function decodeEntities(s) {
|
|
@@ -4576,7 +4825,7 @@ var angularXliff2 = {
|
|
|
4576
4825
|
if (fnameLocale !== void 0 && !LOCALE_RE5.test(fnameLocale)) continue;
|
|
4577
4826
|
let xml;
|
|
4578
4827
|
try {
|
|
4579
|
-
xml =
|
|
4828
|
+
xml = readFileSync15(join11(localeRoot, file), "utf8");
|
|
4580
4829
|
} catch (e) {
|
|
4581
4830
|
warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
|
|
4582
4831
|
continue;
|
|
@@ -4617,8 +4866,8 @@ var angularXliff2 = {
|
|
|
4617
4866
|
};
|
|
4618
4867
|
|
|
4619
4868
|
// src/server/import/parsers/gettext-po.ts
|
|
4620
|
-
import { readdirSync as readdirSync9, readFileSync as
|
|
4621
|
-
import { join as
|
|
4869
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync16 } from "fs";
|
|
4870
|
+
import { join as join12 } from "path";
|
|
4622
4871
|
var LOCALE_RE6 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4623
4872
|
var DIRECTIVE_RE = /^(msgctxt|msgid_plural|msgid|msgstr)(?:\[(\d+)\])?[ \t]+"(.*)"\s*$/;
|
|
4624
4873
|
var CONT_RE = /^[ \t]*"(.*)"\s*$/;
|
|
@@ -4692,17 +4941,17 @@ function discoverPoFiles(root) {
|
|
|
4692
4941
|
for (const e of entries) {
|
|
4693
4942
|
if (e.isFile() && e.name.endsWith(".po")) {
|
|
4694
4943
|
const base = e.name.slice(0, -3);
|
|
4695
|
-
found.push({ path:
|
|
4944
|
+
found.push({ path: join12(root, e.name), rel: e.name, locale: LOCALE_RE6.test(base) ? base : null });
|
|
4696
4945
|
} else if (e.isDirectory() && LOCALE_RE6.test(e.name)) {
|
|
4697
|
-
for (const sub of [
|
|
4946
|
+
for (const sub of [join12(e.name, "LC_MESSAGES"), e.name]) {
|
|
4698
4947
|
let names;
|
|
4699
4948
|
try {
|
|
4700
|
-
names = readdirSync9(
|
|
4949
|
+
names = readdirSync9(join12(root, sub)).sort();
|
|
4701
4950
|
} catch {
|
|
4702
4951
|
continue;
|
|
4703
4952
|
}
|
|
4704
4953
|
for (const f of names) {
|
|
4705
|
-
if (f.endsWith(".po")) found.push({ path:
|
|
4954
|
+
if (f.endsWith(".po")) found.push({ path: join12(root, sub, f), rel: join12(sub, f), locale: e.name });
|
|
4706
4955
|
}
|
|
4707
4956
|
}
|
|
4708
4957
|
}
|
|
@@ -4718,7 +4967,7 @@ var gettextPo2 = {
|
|
|
4718
4967
|
for (const file of discoverPoFiles(localeRoot)) {
|
|
4719
4968
|
let entries;
|
|
4720
4969
|
try {
|
|
4721
|
-
entries = parseEntries(
|
|
4970
|
+
entries = parseEntries(readFileSync16(file.path, "utf8"));
|
|
4722
4971
|
} catch (e) {
|
|
4723
4972
|
warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
|
|
4724
4973
|
continue;
|
|
@@ -4763,8 +5012,8 @@ var gettextPo2 = {
|
|
|
4763
5012
|
};
|
|
4764
5013
|
|
|
4765
5014
|
// src/server/import/parsers/i18next-json.ts
|
|
4766
|
-
import { readdirSync as readdirSync10, readFileSync as
|
|
4767
|
-
import { join as
|
|
5015
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync17, statSync as statSync5 } from "fs";
|
|
5016
|
+
import { join as join13 } from "path";
|
|
4768
5017
|
var LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4769
5018
|
var PLURAL_SUFFIX_RE = /^(.+)_(zero|one|two|few|many|other)$/;
|
|
4770
5019
|
var PLURAL_ARG = "count";
|
|
@@ -4783,7 +5032,7 @@ function fromI18next(value) {
|
|
|
4783
5032
|
function ingestFile(path, label, prefix, locale, keys, warnings) {
|
|
4784
5033
|
let data;
|
|
4785
5034
|
try {
|
|
4786
|
-
data = JSON.parse(
|
|
5035
|
+
data = JSON.parse(readFileSync17(path, "utf8"));
|
|
4787
5036
|
} catch (e) {
|
|
4788
5037
|
warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
|
|
4789
5038
|
return false;
|
|
@@ -4825,7 +5074,7 @@ var i18nextJson2 = {
|
|
|
4825
5074
|
const keys = {};
|
|
4826
5075
|
const locales = [];
|
|
4827
5076
|
for (const entry of readdirSync10(localeRoot).sort()) {
|
|
4828
|
-
const full =
|
|
5077
|
+
const full = join13(localeRoot, entry);
|
|
4829
5078
|
if (safeIsDir2(full)) {
|
|
4830
5079
|
if (!LOCALE_RE7.test(entry)) continue;
|
|
4831
5080
|
if (opts?.locales && !opts.locales.includes(entry)) continue;
|
|
@@ -4834,7 +5083,7 @@ var i18nextJson2 = {
|
|
|
4834
5083
|
if (!file.endsWith(".json")) continue;
|
|
4835
5084
|
const ns = file.slice(0, -".json".length);
|
|
4836
5085
|
const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
|
|
4837
|
-
if (ingestFile(
|
|
5086
|
+
if (ingestFile(join13(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
|
|
4838
5087
|
}
|
|
4839
5088
|
if (any && !locales.includes(entry)) locales.push(entry);
|
|
4840
5089
|
} else if (entry.endsWith(".json")) {
|
|
@@ -4851,8 +5100,8 @@ var i18nextJson2 = {
|
|
|
4851
5100
|
};
|
|
4852
5101
|
|
|
4853
5102
|
// src/server/import/parsers/rails-yaml.ts
|
|
4854
|
-
import { readdirSync as readdirSync11, readFileSync as
|
|
4855
|
-
import { join as
|
|
5103
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync18 } from "fs";
|
|
5104
|
+
import { join as join14 } from "path";
|
|
4856
5105
|
var LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
|
|
4857
5106
|
var CATEGORY_SET = new Set(PLURAL_CATEGORIES);
|
|
4858
5107
|
function fromRuby(value) {
|
|
@@ -5064,7 +5313,7 @@ var railsYaml2 = {
|
|
|
5064
5313
|
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
5065
5314
|
let text;
|
|
5066
5315
|
try {
|
|
5067
|
-
text =
|
|
5316
|
+
text = readFileSync18(join14(localeRoot, file), "utf8");
|
|
5068
5317
|
} catch (e) {
|
|
5069
5318
|
warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
|
|
5070
5319
|
continue;
|
|
@@ -5085,8 +5334,8 @@ var railsYaml2 = {
|
|
|
5085
5334
|
};
|
|
5086
5335
|
|
|
5087
5336
|
// src/server/import/parsers/apple-stringsdict.ts
|
|
5088
|
-
import { readdirSync as readdirSync12, readFileSync as
|
|
5089
|
-
import { join as
|
|
5337
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync19, statSync as statSync6 } from "fs";
|
|
5338
|
+
import { join as join15 } from "path";
|
|
5090
5339
|
var LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5091
5340
|
var TABLE2 = "Localizable.stringsdict";
|
|
5092
5341
|
function localeFromLproj2(dir) {
|
|
@@ -5222,16 +5471,16 @@ var appleStringsdict2 = {
|
|
|
5222
5471
|
const locale = localeFromLproj2(dir);
|
|
5223
5472
|
if (!locale) continue;
|
|
5224
5473
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5225
|
-
const file =
|
|
5474
|
+
const file = join15(localeRoot, dir, TABLE2);
|
|
5226
5475
|
let text;
|
|
5227
5476
|
try {
|
|
5228
5477
|
if (!statSync6(file).isFile()) continue;
|
|
5229
|
-
text =
|
|
5478
|
+
text = readFileSync19(file, "utf8");
|
|
5230
5479
|
} catch {
|
|
5231
5480
|
continue;
|
|
5232
5481
|
}
|
|
5233
5482
|
locales.push(locale);
|
|
5234
|
-
const others = readdirSync12(
|
|
5483
|
+
const others = readdirSync12(join15(localeRoot, dir)).filter(
|
|
5235
5484
|
(f) => f.endsWith(".stringsdict") && f !== TABLE2
|
|
5236
5485
|
);
|
|
5237
5486
|
if (others.length) {
|
|
@@ -5404,7 +5653,7 @@ function runImport(opts) {
|
|
|
5404
5653
|
}
|
|
5405
5654
|
|
|
5406
5655
|
// src/server/export-run.ts
|
|
5407
|
-
import { existsSync as
|
|
5656
|
+
import { existsSync as existsSync12, readFileSync as readFileSync20, readdirSync as readdirSync13, rmdirSync, statSync as statSync7, unlinkSync } from "fs";
|
|
5408
5657
|
import { dirname as dirname2, resolve as resolve7, sep } from "path";
|
|
5409
5658
|
function effectiveLocales(config) {
|
|
5410
5659
|
const limit = config.exportLocales;
|
|
@@ -5447,7 +5696,7 @@ function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
|
|
|
5447
5696
|
if (!segment.includes("{locale}") && !segment.includes("{namespace}")) {
|
|
5448
5697
|
const next = resolve7(dir, segment);
|
|
5449
5698
|
if (isLast) {
|
|
5450
|
-
if (stale(locale) &&
|
|
5699
|
+
if (stale(locale) && existsSync12(next) && statSync7(next).isFile()) {
|
|
5451
5700
|
unlinkSync(next);
|
|
5452
5701
|
deleted++;
|
|
5453
5702
|
removeEmptyDirs(dir, root);
|
|
@@ -5503,7 +5752,7 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
5503
5752
|
writtenPaths.add(abs);
|
|
5504
5753
|
let current = null;
|
|
5505
5754
|
try {
|
|
5506
|
-
current =
|
|
5755
|
+
current = readFileSync20(abs, "utf8");
|
|
5507
5756
|
} catch {
|
|
5508
5757
|
}
|
|
5509
5758
|
if (current === f.contents) {
|
|
@@ -5520,17 +5769,17 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
5520
5769
|
}
|
|
5521
5770
|
|
|
5522
5771
|
// src/server/ui-prefs.ts
|
|
5523
|
-
import { readFileSync as
|
|
5772
|
+
import { readFileSync as readFileSync21 } from "fs";
|
|
5524
5773
|
import { homedir } from "os";
|
|
5525
|
-
import { join as
|
|
5774
|
+
import { join as join16 } from "path";
|
|
5526
5775
|
var THEMES = ["system", "light", "dark"];
|
|
5527
5776
|
var isThemeMode = (v) => THEMES.includes(v);
|
|
5528
5777
|
var isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
|
|
5529
|
-
var defaultUiPrefsPath = () =>
|
|
5778
|
+
var defaultUiPrefsPath = () => join16(homedir(), ".glotfile", "ui.json");
|
|
5530
5779
|
var DEFAULTS = { theme: "system" };
|
|
5531
5780
|
function readJson(path) {
|
|
5532
5781
|
try {
|
|
5533
|
-
const parsed = JSON.parse(
|
|
5782
|
+
const parsed = JSON.parse(readFileSync21(path, "utf8"));
|
|
5534
5783
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
5535
5784
|
} catch {
|
|
5536
5785
|
return {};
|
|
@@ -5549,7 +5798,7 @@ function saveUiPrefs(path, prefs) {
|
|
|
5549
5798
|
}
|
|
5550
5799
|
|
|
5551
5800
|
// src/server/local-settings.ts
|
|
5552
|
-
import { readFileSync as
|
|
5801
|
+
import { readFileSync as readFileSync22 } from "fs";
|
|
5553
5802
|
import { resolve as resolve8 } from "path";
|
|
5554
5803
|
var EDITOR_IDS = ["vscode", "zed", "phpstorm"];
|
|
5555
5804
|
var isEditorId = (v) => EDITOR_IDS.includes(v);
|
|
@@ -5564,7 +5813,7 @@ var DEFAULT_EDITOR = "vscode";
|
|
|
5564
5813
|
var settingsPath = (projectRoot) => resolve8(projectRoot, ".glotfile", "settings.json");
|
|
5565
5814
|
function readJson2(path) {
|
|
5566
5815
|
try {
|
|
5567
|
-
const parsed = JSON.parse(
|
|
5816
|
+
const parsed = JSON.parse(readFileSync22(path, "utf8"));
|
|
5568
5817
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
5569
5818
|
} catch {
|
|
5570
5819
|
return {};
|
|
@@ -5635,15 +5884,30 @@ var sanitize = (s) => s.replace(/[^\w.\-]+/g, "_");
|
|
|
5635
5884
|
var screenshotDirName = (statePath) => basename(statePath).replace(/\.[^.]+$/, "") + "-screenshots";
|
|
5636
5885
|
function projectName(root) {
|
|
5637
5886
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
5638
|
-
if (
|
|
5887
|
+
if (existsSync13(nameFile)) {
|
|
5639
5888
|
try {
|
|
5640
|
-
const name =
|
|
5889
|
+
const name = readFileSync23(nameFile, "utf8").trim();
|
|
5641
5890
|
if (name) return name;
|
|
5642
5891
|
} catch {
|
|
5643
5892
|
}
|
|
5644
5893
|
}
|
|
5645
5894
|
return basename(root);
|
|
5646
5895
|
}
|
|
5896
|
+
function attachUsageSnippets(targets, cache2, projectRoot) {
|
|
5897
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
5898
|
+
for (const target of targets) {
|
|
5899
|
+
const allRefs = Object.entries(cache2.files).flatMap(
|
|
5900
|
+
([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
5901
|
+
key: r.key,
|
|
5902
|
+
file,
|
|
5903
|
+
line: r.line,
|
|
5904
|
+
col: r.col,
|
|
5905
|
+
scanner: r.scanner
|
|
5906
|
+
}))
|
|
5907
|
+
);
|
|
5908
|
+
target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
|
|
5909
|
+
}
|
|
5910
|
+
}
|
|
5647
5911
|
function createApi(deps) {
|
|
5648
5912
|
const app = new Hono();
|
|
5649
5913
|
const load = () => loadState(deps.statePath);
|
|
@@ -5770,7 +6034,7 @@ function createApi(deps) {
|
|
|
5770
6034
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
5771
6035
|
const abs = resolve9(dir, name);
|
|
5772
6036
|
let filePath = null;
|
|
5773
|
-
if ((name === "glotfile" || name.endsWith(".glotfile")) &&
|
|
6037
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync13(resolve9(abs, "config.json"))) {
|
|
5774
6038
|
filePath = resolve9(dir, `${name}.json`);
|
|
5775
6039
|
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
5776
6040
|
filePath = abs;
|
|
@@ -5804,7 +6068,7 @@ function createApi(deps) {
|
|
|
5804
6068
|
const resolved = resolve9(projectRoot, path);
|
|
5805
6069
|
const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
|
|
5806
6070
|
if (!inside) return c.json({ error: "file is outside the project" }, 400);
|
|
5807
|
-
if (!
|
|
6071
|
+
if (!existsSync13(resolved)) return c.json({ error: "file not found" }, 400);
|
|
5808
6072
|
loadState(resolved);
|
|
5809
6073
|
deps.statePath = resolved;
|
|
5810
6074
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
@@ -5865,9 +6129,9 @@ function createApi(deps) {
|
|
|
5865
6129
|
const abs = resolve9(root, screenshot);
|
|
5866
6130
|
const rel = relative4(root, abs);
|
|
5867
6131
|
const seg0 = rel.split(sep2)[0] ?? "";
|
|
5868
|
-
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") &&
|
|
6132
|
+
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync13(abs)) {
|
|
5869
6133
|
try {
|
|
5870
|
-
|
|
6134
|
+
rmSync6(abs);
|
|
5871
6135
|
} catch {
|
|
5872
6136
|
}
|
|
5873
6137
|
}
|
|
@@ -6293,6 +6557,7 @@ function createApi(deps) {
|
|
|
6293
6557
|
persist(fresh);
|
|
6294
6558
|
totalWritten += written;
|
|
6295
6559
|
allErrors.push(...errors);
|
|
6560
|
+
const usage = provider.takeUsage?.();
|
|
6296
6561
|
appendLog(projectRoot, {
|
|
6297
6562
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6298
6563
|
kind: "translate",
|
|
@@ -6303,7 +6568,9 @@ function createApi(deps) {
|
|
|
6303
6568
|
const req = reqById.get(r.id);
|
|
6304
6569
|
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 };
|
|
6305
6570
|
}),
|
|
6306
|
-
results: batchResults
|
|
6571
|
+
results: batchResults,
|
|
6572
|
+
usage,
|
|
6573
|
+
estimatedCostUsd: usageCostUsd(usage, aiCfg)
|
|
6307
6574
|
});
|
|
6308
6575
|
const ld = (localeDone.get(locale) ?? 0) + batchResults.length;
|
|
6309
6576
|
localeDone.set(locale, ld);
|
|
@@ -6375,11 +6642,14 @@ function createApi(deps) {
|
|
|
6375
6642
|
}, aiCfg.concurrency, void 0, aiCfg.batchSize);
|
|
6376
6643
|
const latest = load();
|
|
6377
6644
|
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
6645
|
+
const usage = provider.takeUsage?.();
|
|
6378
6646
|
const entry = {
|
|
6379
6647
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6380
6648
|
kind: "translate",
|
|
6381
6649
|
summary: `Translated ${toTranslate.length} item(s)`,
|
|
6382
6650
|
model: aiCfg.model,
|
|
6651
|
+
usage,
|
|
6652
|
+
estimatedCostUsd: usageCostUsd(usage, aiCfg),
|
|
6383
6653
|
system: buildSystemPrompt(toTranslate.some((r) => r.plural !== void 0)),
|
|
6384
6654
|
// Log the screenshot PATH only — never the image bytes.
|
|
6385
6655
|
items: toTranslate.map((r) => ({
|
|
@@ -6477,17 +6747,7 @@ function createApi(deps) {
|
|
|
6477
6747
|
if (!supportsBatchTranslate(provider)) {
|
|
6478
6748
|
return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
|
|
6479
6749
|
}
|
|
6480
|
-
const outcome = await applyBatchResults(load, persist, provider, pending, projectRoot,
|
|
6481
|
-
batchSize: aiCfg.batchSize,
|
|
6482
|
-
concurrency: aiCfg.concurrency
|
|
6483
|
-
});
|
|
6484
|
-
appendLog(projectRoot, {
|
|
6485
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6486
|
-
kind: "translate",
|
|
6487
|
-
summary: `Applied batch ${pending.batchId}: wrote ${outcome.written}, ${outcome.retried} retried, ${outcome.staleSkipped} stale`,
|
|
6488
|
-
model: aiCfg.model,
|
|
6489
|
-
results: []
|
|
6490
|
-
});
|
|
6750
|
+
const outcome = await applyBatchResults(load, persist, provider, pending, projectRoot, aiCfg);
|
|
6491
6751
|
console.log(`[batch] applied ${pending.batchId} \u2014 wrote ${outcome.written}, ${outcome.errors.length} error(s)`);
|
|
6492
6752
|
return c.json(outcome);
|
|
6493
6753
|
}));
|
|
@@ -6599,19 +6859,7 @@ function createApi(deps) {
|
|
|
6599
6859
|
return;
|
|
6600
6860
|
}
|
|
6601
6861
|
await stream.writeSSE({ event: "start", data: JSON.stringify({ total: targets.length }) });
|
|
6602
|
-
|
|
6603
|
-
for (const target of targets) {
|
|
6604
|
-
const allRefs = Object.entries(cache2.files).flatMap(
|
|
6605
|
-
([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
6606
|
-
key: r.key,
|
|
6607
|
-
file,
|
|
6608
|
-
line: r.line,
|
|
6609
|
-
col: r.col,
|
|
6610
|
-
scanner: r.scanner
|
|
6611
|
-
}))
|
|
6612
|
-
);
|
|
6613
|
-
target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
|
|
6614
|
-
}
|
|
6862
|
+
attachUsageSnippets(targets, cache2, projectRoot);
|
|
6615
6863
|
const system = buildContextSystemPrompt();
|
|
6616
6864
|
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
6617
6865
|
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
@@ -6638,6 +6886,7 @@ function createApi(deps) {
|
|
|
6638
6886
|
const batch = raw;
|
|
6639
6887
|
const fresh = load();
|
|
6640
6888
|
const { written, errors } = applyContext(fresh, chunk2, batch.items ?? [], void 0, body.force === true);
|
|
6889
|
+
const usage = provider.takeUsage?.();
|
|
6641
6890
|
appendLog(projectRoot, {
|
|
6642
6891
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6643
6892
|
kind: "context",
|
|
@@ -6645,7 +6894,9 @@ function createApi(deps) {
|
|
|
6645
6894
|
model: aiCfg.model,
|
|
6646
6895
|
system,
|
|
6647
6896
|
items: chunk2.map((t) => ({ id: t.id, key: t.key, source: t.source })),
|
|
6648
|
-
results: (batch.items ?? []).map((r) => ({ id: r.id, value: r.context, error: r.error }))
|
|
6897
|
+
results: (batch.items ?? []).map((r) => ({ id: r.id, value: r.context, error: r.error })),
|
|
6898
|
+
usage,
|
|
6899
|
+
estimatedCostUsd: usageCostUsd(usage, aiCfg)
|
|
6649
6900
|
});
|
|
6650
6901
|
persist(fresh);
|
|
6651
6902
|
totalWritten += written;
|
|
@@ -6660,6 +6911,100 @@ function createApi(deps) {
|
|
|
6660
6911
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: targets.length, written: totalWritten, errors: allErrors }) });
|
|
6661
6912
|
});
|
|
6662
6913
|
});
|
|
6914
|
+
app.get("/context/batch/status", async (c) => {
|
|
6915
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
6916
|
+
let supported = false;
|
|
6917
|
+
let provider;
|
|
6918
|
+
try {
|
|
6919
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
6920
|
+
supported = supportsBatchComplete(provider);
|
|
6921
|
+
} catch {
|
|
6922
|
+
}
|
|
6923
|
+
const pending = loadPendingContextBatch(projectRoot);
|
|
6924
|
+
if (!pending) return c.json({ supported, pending: null });
|
|
6925
|
+
const base = { batchId: pending.batchId, createdAt: pending.createdAt, model: pending.model, total: pending.total };
|
|
6926
|
+
if (!provider || !supportsBatchComplete(provider)) {
|
|
6927
|
+
return c.json({ supported, pending: { ...base, status: "unknown", counts: null } });
|
|
6928
|
+
}
|
|
6929
|
+
try {
|
|
6930
|
+
const status = await provider.translationBatchStatus(pending.batchId);
|
|
6931
|
+
return c.json({ supported, pending: { ...base, status: status.status, counts: status.counts } });
|
|
6932
|
+
} catch (e) {
|
|
6933
|
+
return c.json({ supported, pending: { ...base, status: "unknown", counts: null, error: e.message } });
|
|
6934
|
+
}
|
|
6935
|
+
});
|
|
6936
|
+
app.post("/context/batch", (c) => withTranslateLock(async () => {
|
|
6937
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6938
|
+
const s = load();
|
|
6939
|
+
const cache2 = loadUsageCache(projectRoot);
|
|
6940
|
+
if (!cache2) return c.json({ error: "No usage index found. Run 'glotfile scan' first." }, 400);
|
|
6941
|
+
const targets = selectContextTargets(s, {
|
|
6942
|
+
all: body.all,
|
|
6943
|
+
keyGlob: body.keyGlob,
|
|
6944
|
+
limit: body.limit,
|
|
6945
|
+
since: body.since,
|
|
6946
|
+
keys: body.keys,
|
|
6947
|
+
force: body.force
|
|
6948
|
+
}, cache2, body.lastRunAt);
|
|
6949
|
+
if (!targets.length) return c.json({ error: "Nothing to build." }, 400);
|
|
6950
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
6951
|
+
let provider;
|
|
6952
|
+
try {
|
|
6953
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
6954
|
+
} catch (e) {
|
|
6955
|
+
return c.json({ error: e.message }, 400);
|
|
6956
|
+
}
|
|
6957
|
+
if (!supportsBatchComplete(provider)) {
|
|
6958
|
+
return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
|
|
6959
|
+
}
|
|
6960
|
+
attachUsageSnippets(targets, cache2, projectRoot);
|
|
6961
|
+
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
6962
|
+
let pending;
|
|
6963
|
+
try {
|
|
6964
|
+
pending = await submitContextBatch(provider, targets, batchSize, aiCfg.model, projectRoot, body.force === true);
|
|
6965
|
+
} catch (e) {
|
|
6966
|
+
return c.json({ error: e.message }, 409);
|
|
6967
|
+
}
|
|
6968
|
+
appendLog(projectRoot, {
|
|
6969
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6970
|
+
kind: "context",
|
|
6971
|
+
summary: `Submitted context batch ${pending.batchId} (${pending.total} keys)`,
|
|
6972
|
+
model: aiCfg.model,
|
|
6973
|
+
system: buildContextSystemPrompt(),
|
|
6974
|
+
items: targets.map((t) => ({ id: t.id, key: t.key, source: t.source }))
|
|
6975
|
+
});
|
|
6976
|
+
console.log(`[context-batch] submitted ${pending.batchId} \u2014 ${pending.total} key(s)`);
|
|
6977
|
+
return c.json({ batchId: pending.batchId, total: pending.total });
|
|
6978
|
+
}));
|
|
6979
|
+
app.post("/context/batch/apply", (c) => withTranslateLock(async () => {
|
|
6980
|
+
const pending = loadPendingContextBatch(projectRoot);
|
|
6981
|
+
if (!pending) return c.json({ error: "No pending context batch." }, 404);
|
|
6982
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
6983
|
+
let provider;
|
|
6984
|
+
try {
|
|
6985
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
6986
|
+
} catch (e) {
|
|
6987
|
+
return c.json({ error: e.message }, 400);
|
|
6988
|
+
}
|
|
6989
|
+
if (!supportsBatchComplete(provider)) {
|
|
6990
|
+
return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
|
|
6991
|
+
}
|
|
6992
|
+
const outcome = await applyContextBatchResults(load, persist, provider, pending, projectRoot, aiCfg);
|
|
6993
|
+
console.log(`[context-batch] applied ${pending.batchId} \u2014 wrote ${outcome.written}, ${outcome.errors.length} error(s)`);
|
|
6994
|
+
return c.json(outcome);
|
|
6995
|
+
}));
|
|
6996
|
+
app.post("/context/batch/cancel", async (c) => {
|
|
6997
|
+
const pending = loadPendingContextBatch(projectRoot);
|
|
6998
|
+
if (!pending) return c.json({ error: "No pending context batch." }, 404);
|
|
6999
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7000
|
+
try {
|
|
7001
|
+
const provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7002
|
+
if (supportsBatchComplete(provider)) await provider.cancelTranslationBatch(pending.batchId);
|
|
7003
|
+
} catch {
|
|
7004
|
+
}
|
|
7005
|
+
clearPendingContextBatch(projectRoot);
|
|
7006
|
+
return c.json({ canceled: pending.batchId });
|
|
7007
|
+
});
|
|
6663
7008
|
app.onError(
|
|
6664
7009
|
(err, c) => c.json({ error: err.message }, err instanceof GlotfileError ? 400 : 500)
|
|
6665
7010
|
);
|
|
@@ -6668,7 +7013,7 @@ function createApi(deps) {
|
|
|
6668
7013
|
|
|
6669
7014
|
// src/server/server.ts
|
|
6670
7015
|
var here = dirname4(fileURLToPath(import.meta.url));
|
|
6671
|
-
var DEFAULT_UI_DIR =
|
|
7016
|
+
var DEFAULT_UI_DIR = join17(here, "..", "ui");
|
|
6672
7017
|
var MIME = {
|
|
6673
7018
|
".html": "text/html; charset=utf-8",
|
|
6674
7019
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -6722,7 +7067,7 @@ function buildApp(opts) {
|
|
|
6722
7067
|
const file = await readFileResponse(target);
|
|
6723
7068
|
if (file) return file;
|
|
6724
7069
|
}
|
|
6725
|
-
const index = await readFileResponse(
|
|
7070
|
+
const index = await readFileResponse(join17(root, "index.html"));
|
|
6726
7071
|
if (index) return index;
|
|
6727
7072
|
return c.notFound();
|
|
6728
7073
|
});
|