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/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.",
|
|
@@ -3198,10 +3201,13 @@ var AnthropicProvider = class {
|
|
|
3198
3201
|
content.push({ type: "text", text: buildBatchPrompt(batch) });
|
|
3199
3202
|
return content;
|
|
3200
3203
|
}
|
|
3201
|
-
|
|
3202
|
-
|
|
3204
|
+
completionContent(req) {
|
|
3205
|
+
return req.content.map(
|
|
3203
3206
|
(b) => b.type === "image" ? { type: "image", source: { type: "base64", media_type: b.mediaType, data: b.base64 } } : { type: "text", text: b.text ?? "" }
|
|
3204
3207
|
);
|
|
3208
|
+
}
|
|
3209
|
+
async complete(req) {
|
|
3210
|
+
const content = this.completionContent(req);
|
|
3205
3211
|
const res = await this.client.messages.create({
|
|
3206
3212
|
model: this.config.model,
|
|
3207
3213
|
max_tokens: req.maxTokens ?? 8192,
|
|
@@ -3265,6 +3271,40 @@ var AnthropicProvider = class {
|
|
|
3265
3271
|
async cancelTranslationBatch(batchId) {
|
|
3266
3272
|
await this.batchesClient().cancel(batchId);
|
|
3267
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
|
+
}
|
|
3268
3308
|
async callBatch(batch, signal) {
|
|
3269
3309
|
const content = this.buildUserContent(batch);
|
|
3270
3310
|
const res = await this.client.messages.create({
|
|
@@ -3998,6 +4038,133 @@ async function applyBatchResults(load, persist, provider, pending, projectRoot,
|
|
|
3998
4038
|
return { written, errors, staleSkipped: stale.length, retried: retryReqs.length, screenshotsSkipped };
|
|
3999
4039
|
}
|
|
4000
4040
|
|
|
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");
|
|
4046
|
+
}
|
|
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;
|
|
4056
|
+
}
|
|
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.");
|
|
4080
|
+
}
|
|
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 };
|
|
4166
|
+
}
|
|
4167
|
+
|
|
4001
4168
|
// src/server/ai/estimate.ts
|
|
4002
4169
|
var CJK_RE = /[ -鿿가-豈-]/g;
|
|
4003
4170
|
function estimateTokens(text) {
|
|
@@ -4055,8 +4222,8 @@ function estimateTranslation(state, ai, opts) {
|
|
|
4055
4222
|
import { relative as relative3 } from "path";
|
|
4056
4223
|
|
|
4057
4224
|
// src/server/import/detect.ts
|
|
4058
|
-
import { existsSync as
|
|
4059
|
-
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";
|
|
4060
4227
|
var LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4061
4228
|
var VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
|
|
4062
4229
|
function safeIsDir(p) {
|
|
@@ -4067,7 +4234,7 @@ function safeIsDir(p) {
|
|
|
4067
4234
|
}
|
|
4068
4235
|
}
|
|
4069
4236
|
function listDirs(dir) {
|
|
4070
|
-
return readdirSync3(dir).filter((e) => safeIsDir(
|
|
4237
|
+
return readdirSync3(dir).filter((e) => safeIsDir(join6(dir, e)));
|
|
4071
4238
|
}
|
|
4072
4239
|
function fileCount(dir) {
|
|
4073
4240
|
try {
|
|
@@ -4081,23 +4248,23 @@ function pickSource(locales, sizeOf) {
|
|
|
4081
4248
|
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
4082
4249
|
}
|
|
4083
4250
|
function detectLaravel(root) {
|
|
4084
|
-
const localeRoot = [
|
|
4251
|
+
const localeRoot = [join6(root, "resources", "lang"), join6(root, "lang")].find(safeIsDir);
|
|
4085
4252
|
if (!localeRoot) return null;
|
|
4086
4253
|
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
4087
4254
|
if (locales.length === 0) return null;
|
|
4088
|
-
const sourceLocale = pickSource(locales, (loc) => fileCount(
|
|
4255
|
+
const sourceLocale = pickSource(locales, (loc) => fileCount(join6(localeRoot, loc)));
|
|
4089
4256
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
4090
4257
|
}
|
|
4091
4258
|
function detectVue(root, forced = false) {
|
|
4092
4259
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
4093
|
-
const localeRoot =
|
|
4260
|
+
const localeRoot = join6(root, rel);
|
|
4094
4261
|
if (!safeIsDir(localeRoot)) continue;
|
|
4095
4262
|
const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4096
4263
|
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
4097
4264
|
if (enough) {
|
|
4098
4265
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4099
4266
|
try {
|
|
4100
|
-
return statSync2(
|
|
4267
|
+
return statSync2(join6(localeRoot, `${loc}.json`)).size;
|
|
4101
4268
|
} catch {
|
|
4102
4269
|
return 0;
|
|
4103
4270
|
}
|
|
@@ -4109,7 +4276,7 @@ function detectVue(root, forced = false) {
|
|
|
4109
4276
|
}
|
|
4110
4277
|
function detectArb(root) {
|
|
4111
4278
|
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
4112
|
-
const localeRoot =
|
|
4279
|
+
const localeRoot = join6(root, rel);
|
|
4113
4280
|
if (!safeIsDir(localeRoot)) continue;
|
|
4114
4281
|
const locales = readdirSync3(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
4115
4282
|
if (locales.length >= 1) {
|
|
@@ -4119,10 +4286,10 @@ function detectArb(root) {
|
|
|
4119
4286
|
return null;
|
|
4120
4287
|
}
|
|
4121
4288
|
function lprojLocales(dir) {
|
|
4122
|
-
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")));
|
|
4123
4290
|
}
|
|
4124
4291
|
function detectApple(root) {
|
|
4125
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
4292
|
+
const candidates = [root, ...listDirs(root).map((d) => join6(root, d))];
|
|
4126
4293
|
let best = null;
|
|
4127
4294
|
for (const dir of candidates) {
|
|
4128
4295
|
const locales = lprojLocales(dir);
|
|
@@ -4134,7 +4301,7 @@ function detectApple(root) {
|
|
|
4134
4301
|
locales,
|
|
4135
4302
|
sourceLocale: pickSource(locales, (loc) => {
|
|
4136
4303
|
try {
|
|
4137
|
-
return statSync2(
|
|
4304
|
+
return statSync2(join6(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
4138
4305
|
} catch {
|
|
4139
4306
|
return 0;
|
|
4140
4307
|
}
|
|
@@ -4147,7 +4314,7 @@ function detectApple(root) {
|
|
|
4147
4314
|
var ANGULAR_DIR_CANDIDATES = [".", "src/locale", "src/locales", "src/i18n", "locale", "locales", "i18n", "translations"];
|
|
4148
4315
|
function detectAngularXliff(root) {
|
|
4149
4316
|
for (const rel of ANGULAR_DIR_CANDIDATES) {
|
|
4150
|
-
const localeRoot = rel === "." ? root :
|
|
4317
|
+
const localeRoot = rel === "." ? root : join6(root, rel);
|
|
4151
4318
|
if (!safeIsDir(localeRoot)) continue;
|
|
4152
4319
|
const files = readdirSync3(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
|
|
4153
4320
|
if (files.length === 0) continue;
|
|
@@ -4155,7 +4322,7 @@ function detectAngularXliff(root) {
|
|
|
4155
4322
|
const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
|
|
4156
4323
|
let sourceLocale;
|
|
4157
4324
|
try {
|
|
4158
|
-
sourceLocale =
|
|
4325
|
+
sourceLocale = readFileSync11(join6(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
|
|
4159
4326
|
} catch {
|
|
4160
4327
|
}
|
|
4161
4328
|
if (!sourceLocale && locales.length === 0) continue;
|
|
@@ -4166,14 +4333,14 @@ function detectAngularXliff(root) {
|
|
|
4166
4333
|
return null;
|
|
4167
4334
|
}
|
|
4168
4335
|
function detectRails(root) {
|
|
4169
|
-
const localeRoot =
|
|
4336
|
+
const localeRoot = join6(root, "config", "locales");
|
|
4170
4337
|
if (!safeIsDir(localeRoot)) return null;
|
|
4171
4338
|
const locales = [];
|
|
4172
4339
|
for (const file of readdirSync3(localeRoot).sort()) {
|
|
4173
4340
|
if (!/\.ya?ml$/.test(file)) continue;
|
|
4174
4341
|
let text;
|
|
4175
4342
|
try {
|
|
4176
|
-
text =
|
|
4343
|
+
text = readFileSync11(join6(localeRoot, file), "utf8");
|
|
4177
4344
|
} catch {
|
|
4178
4345
|
continue;
|
|
4179
4346
|
}
|
|
@@ -4188,15 +4355,15 @@ function detectRails(root) {
|
|
|
4188
4355
|
var I18NEXT_DIR_CANDIDATES = ["public/locales", "static/locales", "locales", "src/locales", "src/i18n/locales"];
|
|
4189
4356
|
function detectI18next(root) {
|
|
4190
4357
|
for (const rel of I18NEXT_DIR_CANDIDATES) {
|
|
4191
|
-
const localeRoot =
|
|
4358
|
+
const localeRoot = join6(root, rel);
|
|
4192
4359
|
if (!safeIsDir(localeRoot)) continue;
|
|
4193
4360
|
const locales = listDirs(localeRoot).filter(
|
|
4194
|
-
(d) => LOCALE_RE.test(d) && readdirSync3(
|
|
4361
|
+
(d) => LOCALE_RE.test(d) && readdirSync3(join6(localeRoot, d)).some((f) => f.endsWith(".json"))
|
|
4195
4362
|
);
|
|
4196
4363
|
if (locales.length === 0) continue;
|
|
4197
4364
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4198
4365
|
try {
|
|
4199
|
-
return readdirSync3(
|
|
4366
|
+
return readdirSync3(join6(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync2(join6(localeRoot, loc, f)).size, 0);
|
|
4200
4367
|
} catch {
|
|
4201
4368
|
return 0;
|
|
4202
4369
|
}
|
|
@@ -4213,8 +4380,8 @@ function gettextLocales(dir) {
|
|
|
4213
4380
|
if (!locales.includes(flat)) locales.push(flat);
|
|
4214
4381
|
continue;
|
|
4215
4382
|
}
|
|
4216
|
-
if (!LOCALE_RE.test(entry) || !safeIsDir(
|
|
4217
|
-
const sub =
|
|
4383
|
+
if (!LOCALE_RE.test(entry) || !safeIsDir(join6(dir, entry))) continue;
|
|
4384
|
+
const sub = join6(dir, entry);
|
|
4218
4385
|
const hasPo = (d) => {
|
|
4219
4386
|
try {
|
|
4220
4387
|
return readdirSync3(d).some((f) => f.endsWith(".po"));
|
|
@@ -4222,7 +4389,7 @@ function gettextLocales(dir) {
|
|
|
4222
4389
|
return false;
|
|
4223
4390
|
}
|
|
4224
4391
|
};
|
|
4225
|
-
if (hasPo(
|
|
4392
|
+
if (hasPo(join6(sub, "LC_MESSAGES")) || hasPo(sub)) {
|
|
4226
4393
|
if (!locales.includes(entry)) locales.push(entry);
|
|
4227
4394
|
}
|
|
4228
4395
|
}
|
|
@@ -4231,7 +4398,7 @@ function gettextLocales(dir) {
|
|
|
4231
4398
|
var GETTEXT_DIR_CANDIDATES = ["locale", "locales", "po", "translations"];
|
|
4232
4399
|
function detectGettext(root) {
|
|
4233
4400
|
for (const rel of GETTEXT_DIR_CANDIDATES) {
|
|
4234
|
-
const localeRoot =
|
|
4401
|
+
const localeRoot = join6(root, rel);
|
|
4235
4402
|
if (!safeIsDir(localeRoot)) continue;
|
|
4236
4403
|
const locales = gettextLocales(localeRoot);
|
|
4237
4404
|
if (locales.length === 0) continue;
|
|
@@ -4240,10 +4407,10 @@ function detectGettext(root) {
|
|
|
4240
4407
|
return null;
|
|
4241
4408
|
}
|
|
4242
4409
|
function detectAppleStringsdict(root) {
|
|
4243
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
4410
|
+
const candidates = [root, ...listDirs(root).map((d) => join6(root, d))];
|
|
4244
4411
|
let best = null;
|
|
4245
4412
|
for (const dir of candidates) {
|
|
4246
|
-
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")));
|
|
4247
4414
|
if (locales.length === 0) continue;
|
|
4248
4415
|
if (!best || locales.length > best.locales.length) {
|
|
4249
4416
|
best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
@@ -4274,7 +4441,7 @@ var BY_FORMAT = {
|
|
|
4274
4441
|
"apple-stringsdict": detectAppleStringsdict
|
|
4275
4442
|
};
|
|
4276
4443
|
function detect(root, formatOverride) {
|
|
4277
|
-
if (!
|
|
4444
|
+
if (!existsSync11(root)) return null;
|
|
4278
4445
|
if (formatOverride) {
|
|
4279
4446
|
const fn = BY_FORMAT[formatOverride];
|
|
4280
4447
|
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
@@ -4288,8 +4455,8 @@ function detect(root, formatOverride) {
|
|
|
4288
4455
|
}
|
|
4289
4456
|
|
|
4290
4457
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
4291
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
4292
|
-
import { join as
|
|
4458
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync12 } from "fs";
|
|
4459
|
+
import { join as join7 } from "path";
|
|
4293
4460
|
|
|
4294
4461
|
// src/server/import/flatten.ts
|
|
4295
4462
|
function flattenObject(value, prefix, warnings) {
|
|
@@ -4328,7 +4495,7 @@ var vueI18nJson2 = {
|
|
|
4328
4495
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4329
4496
|
let data;
|
|
4330
4497
|
try {
|
|
4331
|
-
data = JSON.parse(
|
|
4498
|
+
data = JSON.parse(readFileSync12(join7(localeRoot, file), "utf8"));
|
|
4332
4499
|
} catch (e) {
|
|
4333
4500
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
4334
4501
|
continue;
|
|
@@ -4344,7 +4511,7 @@ var vueI18nJson2 = {
|
|
|
4344
4511
|
|
|
4345
4512
|
// src/server/import/parsers/laravel-php.ts
|
|
4346
4513
|
import { readdirSync as readdirSync5, statSync as statSync3 } from "fs";
|
|
4347
|
-
import { join as
|
|
4514
|
+
import { join as join8, relative as relative2 } from "path";
|
|
4348
4515
|
import { execFileSync } from "child_process";
|
|
4349
4516
|
|
|
4350
4517
|
// src/server/import/placeholders.ts
|
|
@@ -4354,13 +4521,13 @@ function laravelToCanonical(value) {
|
|
|
4354
4521
|
|
|
4355
4522
|
// src/server/import/parsers/laravel-php.ts
|
|
4356
4523
|
function listDirs2(dir) {
|
|
4357
|
-
return readdirSync5(dir).filter((e) => statSync3(
|
|
4524
|
+
return readdirSync5(dir).filter((e) => statSync3(join8(dir, e)).isDirectory());
|
|
4358
4525
|
}
|
|
4359
4526
|
function listPhpFiles(dir) {
|
|
4360
4527
|
const out = [];
|
|
4361
4528
|
const walk = (d) => {
|
|
4362
4529
|
for (const e of readdirSync5(d)) {
|
|
4363
|
-
const full =
|
|
4530
|
+
const full = join8(d, e);
|
|
4364
4531
|
if (statSync3(full).isDirectory()) walk(full);
|
|
4365
4532
|
else if (e.endsWith(".php")) out.push(full);
|
|
4366
4533
|
}
|
|
@@ -4397,7 +4564,7 @@ var laravelPhp2 = {
|
|
|
4397
4564
|
for (const locale of listDirs2(localeRoot).sort()) {
|
|
4398
4565
|
if (locale === "vendor") continue;
|
|
4399
4566
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4400
|
-
const localeDir =
|
|
4567
|
+
const localeDir = join8(localeRoot, locale);
|
|
4401
4568
|
locales.push(locale);
|
|
4402
4569
|
for (const file of listPhpFiles(localeDir)) {
|
|
4403
4570
|
const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
|
|
@@ -4420,8 +4587,8 @@ var laravelPhp2 = {
|
|
|
4420
4587
|
};
|
|
4421
4588
|
|
|
4422
4589
|
// src/server/import/parsers/flutter-arb.ts
|
|
4423
|
-
import { readdirSync as readdirSync6, readFileSync as
|
|
4424
|
-
import { join as
|
|
4590
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync13 } from "fs";
|
|
4591
|
+
import { join as join9 } from "path";
|
|
4425
4592
|
var LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4426
4593
|
function localeFromArbName(file) {
|
|
4427
4594
|
const m = file.match(/^(.+)\.arb$/);
|
|
@@ -4457,7 +4624,7 @@ var flutterArb2 = {
|
|
|
4457
4624
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4458
4625
|
let data;
|
|
4459
4626
|
try {
|
|
4460
|
-
data = JSON.parse(
|
|
4627
|
+
data = JSON.parse(readFileSync13(join9(localeRoot, file), "utf8"));
|
|
4461
4628
|
} catch (e) {
|
|
4462
4629
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
4463
4630
|
continue;
|
|
@@ -4482,8 +4649,8 @@ var flutterArb2 = {
|
|
|
4482
4649
|
};
|
|
4483
4650
|
|
|
4484
4651
|
// src/server/import/parsers/apple-strings.ts
|
|
4485
|
-
import { readdirSync as readdirSync7, readFileSync as
|
|
4486
|
-
import { join as
|
|
4652
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync14, statSync as statSync4 } from "fs";
|
|
4653
|
+
import { join as join10 } from "path";
|
|
4487
4654
|
var LOCALE_RE4 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4488
4655
|
var TABLE = "Localizable.strings";
|
|
4489
4656
|
function localeFromLproj(dir) {
|
|
@@ -4587,16 +4754,16 @@ var appleStrings2 = {
|
|
|
4587
4754
|
const locale = localeFromLproj(dir);
|
|
4588
4755
|
if (!locale) continue;
|
|
4589
4756
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4590
|
-
const file =
|
|
4757
|
+
const file = join10(localeRoot, dir, TABLE);
|
|
4591
4758
|
let text;
|
|
4592
4759
|
try {
|
|
4593
4760
|
if (!statSync4(file).isFile()) continue;
|
|
4594
|
-
text =
|
|
4761
|
+
text = readFileSync14(file, "utf8");
|
|
4595
4762
|
} catch {
|
|
4596
4763
|
continue;
|
|
4597
4764
|
}
|
|
4598
4765
|
locales.push(locale);
|
|
4599
|
-
const others = readdirSync7(
|
|
4766
|
+
const others = readdirSync7(join10(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
|
|
4600
4767
|
if (others.length) {
|
|
4601
4768
|
warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
|
|
4602
4769
|
}
|
|
@@ -4609,8 +4776,8 @@ var appleStrings2 = {
|
|
|
4609
4776
|
};
|
|
4610
4777
|
|
|
4611
4778
|
// src/server/import/parsers/angular-xliff.ts
|
|
4612
|
-
import { readdirSync as readdirSync8, readFileSync as
|
|
4613
|
-
import { join as
|
|
4779
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync15 } from "fs";
|
|
4780
|
+
import { join as join11 } from "path";
|
|
4614
4781
|
var LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4615
4782
|
var FILE_RE = /^messages(?:\.(.+))?\.xlf$/;
|
|
4616
4783
|
function decodeEntities(s) {
|
|
@@ -4658,7 +4825,7 @@ var angularXliff2 = {
|
|
|
4658
4825
|
if (fnameLocale !== void 0 && !LOCALE_RE5.test(fnameLocale)) continue;
|
|
4659
4826
|
let xml;
|
|
4660
4827
|
try {
|
|
4661
|
-
xml =
|
|
4828
|
+
xml = readFileSync15(join11(localeRoot, file), "utf8");
|
|
4662
4829
|
} catch (e) {
|
|
4663
4830
|
warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
|
|
4664
4831
|
continue;
|
|
@@ -4699,8 +4866,8 @@ var angularXliff2 = {
|
|
|
4699
4866
|
};
|
|
4700
4867
|
|
|
4701
4868
|
// src/server/import/parsers/gettext-po.ts
|
|
4702
|
-
import { readdirSync as readdirSync9, readFileSync as
|
|
4703
|
-
import { join as
|
|
4869
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync16 } from "fs";
|
|
4870
|
+
import { join as join12 } from "path";
|
|
4704
4871
|
var LOCALE_RE6 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4705
4872
|
var DIRECTIVE_RE = /^(msgctxt|msgid_plural|msgid|msgstr)(?:\[(\d+)\])?[ \t]+"(.*)"\s*$/;
|
|
4706
4873
|
var CONT_RE = /^[ \t]*"(.*)"\s*$/;
|
|
@@ -4774,17 +4941,17 @@ function discoverPoFiles(root) {
|
|
|
4774
4941
|
for (const e of entries) {
|
|
4775
4942
|
if (e.isFile() && e.name.endsWith(".po")) {
|
|
4776
4943
|
const base = e.name.slice(0, -3);
|
|
4777
|
-
found.push({ path:
|
|
4944
|
+
found.push({ path: join12(root, e.name), rel: e.name, locale: LOCALE_RE6.test(base) ? base : null });
|
|
4778
4945
|
} else if (e.isDirectory() && LOCALE_RE6.test(e.name)) {
|
|
4779
|
-
for (const sub of [
|
|
4946
|
+
for (const sub of [join12(e.name, "LC_MESSAGES"), e.name]) {
|
|
4780
4947
|
let names;
|
|
4781
4948
|
try {
|
|
4782
|
-
names = readdirSync9(
|
|
4949
|
+
names = readdirSync9(join12(root, sub)).sort();
|
|
4783
4950
|
} catch {
|
|
4784
4951
|
continue;
|
|
4785
4952
|
}
|
|
4786
4953
|
for (const f of names) {
|
|
4787
|
-
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 });
|
|
4788
4955
|
}
|
|
4789
4956
|
}
|
|
4790
4957
|
}
|
|
@@ -4800,7 +4967,7 @@ var gettextPo2 = {
|
|
|
4800
4967
|
for (const file of discoverPoFiles(localeRoot)) {
|
|
4801
4968
|
let entries;
|
|
4802
4969
|
try {
|
|
4803
|
-
entries = parseEntries(
|
|
4970
|
+
entries = parseEntries(readFileSync16(file.path, "utf8"));
|
|
4804
4971
|
} catch (e) {
|
|
4805
4972
|
warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
|
|
4806
4973
|
continue;
|
|
@@ -4845,8 +5012,8 @@ var gettextPo2 = {
|
|
|
4845
5012
|
};
|
|
4846
5013
|
|
|
4847
5014
|
// src/server/import/parsers/i18next-json.ts
|
|
4848
|
-
import { readdirSync as readdirSync10, readFileSync as
|
|
4849
|
-
import { join as
|
|
5015
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync17, statSync as statSync5 } from "fs";
|
|
5016
|
+
import { join as join13 } from "path";
|
|
4850
5017
|
var LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4851
5018
|
var PLURAL_SUFFIX_RE = /^(.+)_(zero|one|two|few|many|other)$/;
|
|
4852
5019
|
var PLURAL_ARG = "count";
|
|
@@ -4865,7 +5032,7 @@ function fromI18next(value) {
|
|
|
4865
5032
|
function ingestFile(path, label, prefix, locale, keys, warnings) {
|
|
4866
5033
|
let data;
|
|
4867
5034
|
try {
|
|
4868
|
-
data = JSON.parse(
|
|
5035
|
+
data = JSON.parse(readFileSync17(path, "utf8"));
|
|
4869
5036
|
} catch (e) {
|
|
4870
5037
|
warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
|
|
4871
5038
|
return false;
|
|
@@ -4907,7 +5074,7 @@ var i18nextJson2 = {
|
|
|
4907
5074
|
const keys = {};
|
|
4908
5075
|
const locales = [];
|
|
4909
5076
|
for (const entry of readdirSync10(localeRoot).sort()) {
|
|
4910
|
-
const full =
|
|
5077
|
+
const full = join13(localeRoot, entry);
|
|
4911
5078
|
if (safeIsDir2(full)) {
|
|
4912
5079
|
if (!LOCALE_RE7.test(entry)) continue;
|
|
4913
5080
|
if (opts?.locales && !opts.locales.includes(entry)) continue;
|
|
@@ -4916,7 +5083,7 @@ var i18nextJson2 = {
|
|
|
4916
5083
|
if (!file.endsWith(".json")) continue;
|
|
4917
5084
|
const ns = file.slice(0, -".json".length);
|
|
4918
5085
|
const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
|
|
4919
|
-
if (ingestFile(
|
|
5086
|
+
if (ingestFile(join13(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
|
|
4920
5087
|
}
|
|
4921
5088
|
if (any && !locales.includes(entry)) locales.push(entry);
|
|
4922
5089
|
} else if (entry.endsWith(".json")) {
|
|
@@ -4933,8 +5100,8 @@ var i18nextJson2 = {
|
|
|
4933
5100
|
};
|
|
4934
5101
|
|
|
4935
5102
|
// src/server/import/parsers/rails-yaml.ts
|
|
4936
|
-
import { readdirSync as readdirSync11, readFileSync as
|
|
4937
|
-
import { join as
|
|
5103
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync18 } from "fs";
|
|
5104
|
+
import { join as join14 } from "path";
|
|
4938
5105
|
var LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
|
|
4939
5106
|
var CATEGORY_SET = new Set(PLURAL_CATEGORIES);
|
|
4940
5107
|
function fromRuby(value) {
|
|
@@ -5146,7 +5313,7 @@ var railsYaml2 = {
|
|
|
5146
5313
|
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
5147
5314
|
let text;
|
|
5148
5315
|
try {
|
|
5149
|
-
text =
|
|
5316
|
+
text = readFileSync18(join14(localeRoot, file), "utf8");
|
|
5150
5317
|
} catch (e) {
|
|
5151
5318
|
warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
|
|
5152
5319
|
continue;
|
|
@@ -5167,8 +5334,8 @@ var railsYaml2 = {
|
|
|
5167
5334
|
};
|
|
5168
5335
|
|
|
5169
5336
|
// src/server/import/parsers/apple-stringsdict.ts
|
|
5170
|
-
import { readdirSync as readdirSync12, readFileSync as
|
|
5171
|
-
import { join as
|
|
5337
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync19, statSync as statSync6 } from "fs";
|
|
5338
|
+
import { join as join15 } from "path";
|
|
5172
5339
|
var LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5173
5340
|
var TABLE2 = "Localizable.stringsdict";
|
|
5174
5341
|
function localeFromLproj2(dir) {
|
|
@@ -5304,16 +5471,16 @@ var appleStringsdict2 = {
|
|
|
5304
5471
|
const locale = localeFromLproj2(dir);
|
|
5305
5472
|
if (!locale) continue;
|
|
5306
5473
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5307
|
-
const file =
|
|
5474
|
+
const file = join15(localeRoot, dir, TABLE2);
|
|
5308
5475
|
let text;
|
|
5309
5476
|
try {
|
|
5310
5477
|
if (!statSync6(file).isFile()) continue;
|
|
5311
|
-
text =
|
|
5478
|
+
text = readFileSync19(file, "utf8");
|
|
5312
5479
|
} catch {
|
|
5313
5480
|
continue;
|
|
5314
5481
|
}
|
|
5315
5482
|
locales.push(locale);
|
|
5316
|
-
const others = readdirSync12(
|
|
5483
|
+
const others = readdirSync12(join15(localeRoot, dir)).filter(
|
|
5317
5484
|
(f) => f.endsWith(".stringsdict") && f !== TABLE2
|
|
5318
5485
|
);
|
|
5319
5486
|
if (others.length) {
|
|
@@ -5486,7 +5653,7 @@ function runImport(opts) {
|
|
|
5486
5653
|
}
|
|
5487
5654
|
|
|
5488
5655
|
// src/server/export-run.ts
|
|
5489
|
-
import { existsSync as
|
|
5656
|
+
import { existsSync as existsSync12, readFileSync as readFileSync20, readdirSync as readdirSync13, rmdirSync, statSync as statSync7, unlinkSync } from "fs";
|
|
5490
5657
|
import { dirname as dirname2, resolve as resolve7, sep } from "path";
|
|
5491
5658
|
function effectiveLocales(config) {
|
|
5492
5659
|
const limit = config.exportLocales;
|
|
@@ -5529,7 +5696,7 @@ function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
|
|
|
5529
5696
|
if (!segment.includes("{locale}") && !segment.includes("{namespace}")) {
|
|
5530
5697
|
const next = resolve7(dir, segment);
|
|
5531
5698
|
if (isLast) {
|
|
5532
|
-
if (stale(locale) &&
|
|
5699
|
+
if (stale(locale) && existsSync12(next) && statSync7(next).isFile()) {
|
|
5533
5700
|
unlinkSync(next);
|
|
5534
5701
|
deleted++;
|
|
5535
5702
|
removeEmptyDirs(dir, root);
|
|
@@ -5585,7 +5752,7 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
5585
5752
|
writtenPaths.add(abs);
|
|
5586
5753
|
let current = null;
|
|
5587
5754
|
try {
|
|
5588
|
-
current =
|
|
5755
|
+
current = readFileSync20(abs, "utf8");
|
|
5589
5756
|
} catch {
|
|
5590
5757
|
}
|
|
5591
5758
|
if (current === f.contents) {
|
|
@@ -5602,17 +5769,17 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
5602
5769
|
}
|
|
5603
5770
|
|
|
5604
5771
|
// src/server/ui-prefs.ts
|
|
5605
|
-
import { readFileSync as
|
|
5772
|
+
import { readFileSync as readFileSync21 } from "fs";
|
|
5606
5773
|
import { homedir } from "os";
|
|
5607
|
-
import { join as
|
|
5774
|
+
import { join as join16 } from "path";
|
|
5608
5775
|
var THEMES = ["system", "light", "dark"];
|
|
5609
5776
|
var isThemeMode = (v) => THEMES.includes(v);
|
|
5610
5777
|
var isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
|
|
5611
|
-
var defaultUiPrefsPath = () =>
|
|
5778
|
+
var defaultUiPrefsPath = () => join16(homedir(), ".glotfile", "ui.json");
|
|
5612
5779
|
var DEFAULTS = { theme: "system" };
|
|
5613
5780
|
function readJson(path) {
|
|
5614
5781
|
try {
|
|
5615
|
-
const parsed = JSON.parse(
|
|
5782
|
+
const parsed = JSON.parse(readFileSync21(path, "utf8"));
|
|
5616
5783
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
5617
5784
|
} catch {
|
|
5618
5785
|
return {};
|
|
@@ -5631,7 +5798,7 @@ function saveUiPrefs(path, prefs) {
|
|
|
5631
5798
|
}
|
|
5632
5799
|
|
|
5633
5800
|
// src/server/local-settings.ts
|
|
5634
|
-
import { readFileSync as
|
|
5801
|
+
import { readFileSync as readFileSync22 } from "fs";
|
|
5635
5802
|
import { resolve as resolve8 } from "path";
|
|
5636
5803
|
var EDITOR_IDS = ["vscode", "zed", "phpstorm"];
|
|
5637
5804
|
var isEditorId = (v) => EDITOR_IDS.includes(v);
|
|
@@ -5646,7 +5813,7 @@ var DEFAULT_EDITOR = "vscode";
|
|
|
5646
5813
|
var settingsPath = (projectRoot) => resolve8(projectRoot, ".glotfile", "settings.json");
|
|
5647
5814
|
function readJson2(path) {
|
|
5648
5815
|
try {
|
|
5649
|
-
const parsed = JSON.parse(
|
|
5816
|
+
const parsed = JSON.parse(readFileSync22(path, "utf8"));
|
|
5650
5817
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
5651
5818
|
} catch {
|
|
5652
5819
|
return {};
|
|
@@ -5717,15 +5884,30 @@ var sanitize = (s) => s.replace(/[^\w.\-]+/g, "_");
|
|
|
5717
5884
|
var screenshotDirName = (statePath) => basename(statePath).replace(/\.[^.]+$/, "") + "-screenshots";
|
|
5718
5885
|
function projectName(root) {
|
|
5719
5886
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
5720
|
-
if (
|
|
5887
|
+
if (existsSync13(nameFile)) {
|
|
5721
5888
|
try {
|
|
5722
|
-
const name =
|
|
5889
|
+
const name = readFileSync23(nameFile, "utf8").trim();
|
|
5723
5890
|
if (name) return name;
|
|
5724
5891
|
} catch {
|
|
5725
5892
|
}
|
|
5726
5893
|
}
|
|
5727
5894
|
return basename(root);
|
|
5728
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
|
+
}
|
|
5729
5911
|
function createApi(deps) {
|
|
5730
5912
|
const app = new Hono();
|
|
5731
5913
|
const load = () => loadState(deps.statePath);
|
|
@@ -5852,7 +6034,7 @@ function createApi(deps) {
|
|
|
5852
6034
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
5853
6035
|
const abs = resolve9(dir, name);
|
|
5854
6036
|
let filePath = null;
|
|
5855
|
-
if ((name === "glotfile" || name.endsWith(".glotfile")) &&
|
|
6037
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync13(resolve9(abs, "config.json"))) {
|
|
5856
6038
|
filePath = resolve9(dir, `${name}.json`);
|
|
5857
6039
|
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
5858
6040
|
filePath = abs;
|
|
@@ -5886,7 +6068,7 @@ function createApi(deps) {
|
|
|
5886
6068
|
const resolved = resolve9(projectRoot, path);
|
|
5887
6069
|
const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
|
|
5888
6070
|
if (!inside) return c.json({ error: "file is outside the project" }, 400);
|
|
5889
|
-
if (!
|
|
6071
|
+
if (!existsSync13(resolved)) return c.json({ error: "file not found" }, 400);
|
|
5890
6072
|
loadState(resolved);
|
|
5891
6073
|
deps.statePath = resolved;
|
|
5892
6074
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
@@ -5947,9 +6129,9 @@ function createApi(deps) {
|
|
|
5947
6129
|
const abs = resolve9(root, screenshot);
|
|
5948
6130
|
const rel = relative4(root, abs);
|
|
5949
6131
|
const seg0 = rel.split(sep2)[0] ?? "";
|
|
5950
|
-
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") &&
|
|
6132
|
+
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync13(abs)) {
|
|
5951
6133
|
try {
|
|
5952
|
-
|
|
6134
|
+
rmSync6(abs);
|
|
5953
6135
|
} catch {
|
|
5954
6136
|
}
|
|
5955
6137
|
}
|
|
@@ -6677,19 +6859,7 @@ function createApi(deps) {
|
|
|
6677
6859
|
return;
|
|
6678
6860
|
}
|
|
6679
6861
|
await stream.writeSSE({ event: "start", data: JSON.stringify({ total: targets.length }) });
|
|
6680
|
-
|
|
6681
|
-
for (const target of targets) {
|
|
6682
|
-
const allRefs = Object.entries(cache2.files).flatMap(
|
|
6683
|
-
([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
6684
|
-
key: r.key,
|
|
6685
|
-
file,
|
|
6686
|
-
line: r.line,
|
|
6687
|
-
col: r.col,
|
|
6688
|
-
scanner: r.scanner
|
|
6689
|
-
}))
|
|
6690
|
-
);
|
|
6691
|
-
target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
|
|
6692
|
-
}
|
|
6862
|
+
attachUsageSnippets(targets, cache2, projectRoot);
|
|
6693
6863
|
const system = buildContextSystemPrompt();
|
|
6694
6864
|
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
6695
6865
|
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
@@ -6741,6 +6911,100 @@ function createApi(deps) {
|
|
|
6741
6911
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: targets.length, written: totalWritten, errors: allErrors }) });
|
|
6742
6912
|
});
|
|
6743
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
|
+
});
|
|
6744
7008
|
app.onError(
|
|
6745
7009
|
(err, c) => c.json({ error: err.message }, err instanceof GlotfileError ? 400 : 500)
|
|
6746
7010
|
);
|
|
@@ -6749,7 +7013,7 @@ function createApi(deps) {
|
|
|
6749
7013
|
|
|
6750
7014
|
// src/server/server.ts
|
|
6751
7015
|
var here = dirname4(fileURLToPath(import.meta.url));
|
|
6752
|
-
var DEFAULT_UI_DIR =
|
|
7016
|
+
var DEFAULT_UI_DIR = join17(here, "..", "ui");
|
|
6753
7017
|
var MIME = {
|
|
6754
7018
|
".html": "text/html; charset=utf-8",
|
|
6755
7019
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -6803,7 +7067,7 @@ function buildApp(opts) {
|
|
|
6803
7067
|
const file = await readFileResponse(target);
|
|
6804
7068
|
if (file) return file;
|
|
6805
7069
|
}
|
|
6806
|
-
const index = await readFileResponse(
|
|
7070
|
+
const index = await readFileResponse(join17(root, "index.html"));
|
|
6807
7071
|
if (index) return index;
|
|
6808
7072
|
return c.notFound();
|
|
6809
7073
|
});
|