glotfile 0.6.4 → 0.7.1
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 +533 -111
- package/dist/server/server.js +356 -85
- package/dist/ui/assets/index-DNjcY2ek.css +1 -0
- package/dist/ui/assets/index-Dx0VxxJh.js +2124 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BV60Iswg.js +0 -2105
- package/dist/ui/assets/index-C_ML7XO5.css +0 -1
package/dist/server/cli.js
CHANGED
|
@@ -1969,6 +1969,9 @@ var init_export_run = __esm({
|
|
|
1969
1969
|
});
|
|
1970
1970
|
|
|
1971
1971
|
// src/server/ai/provider.ts
|
|
1972
|
+
function supportsBatchTranslate(p) {
|
|
1973
|
+
return typeof p.submitTranslationBatch === "function";
|
|
1974
|
+
}
|
|
1972
1975
|
function buildSystemPrompt(hasPluralItems) {
|
|
1973
1976
|
const lines = [
|
|
1974
1977
|
"You are a professional software localization engine for a UI string catalog.",
|
|
@@ -2277,6 +2280,53 @@ var init_anthropic = __esm({
|
|
|
2277
2280
|
return {};
|
|
2278
2281
|
}
|
|
2279
2282
|
}
|
|
2283
|
+
batchesClient() {
|
|
2284
|
+
const b = this.client.messages.batches;
|
|
2285
|
+
if (!b) throw new Error("Anthropic client has no batches support.");
|
|
2286
|
+
return b;
|
|
2287
|
+
}
|
|
2288
|
+
// Each job becomes one batch entry whose params mirror callBatch exactly —
|
|
2289
|
+
// same prompts, schema, and vision blocks — so batch and sync replies are
|
|
2290
|
+
// interchangeable downstream.
|
|
2291
|
+
async submitTranslationBatch(jobs) {
|
|
2292
|
+
const requests = jobs.map((job) => ({
|
|
2293
|
+
custom_id: job.customId,
|
|
2294
|
+
params: {
|
|
2295
|
+
model: this.config.model,
|
|
2296
|
+
max_tokens: 8192,
|
|
2297
|
+
// Batch entries don't share a live cache window, so cache_control is omitted here.
|
|
2298
|
+
system: [{ type: "text", text: buildSystemPrompt(job.requests.some((r) => r.plural !== void 0)) }],
|
|
2299
|
+
output_config: { format: { type: "json_schema", schema: BATCH_SCHEMA } },
|
|
2300
|
+
messages: [{ role: "user", content: this.buildUserContent(job.requests) }]
|
|
2301
|
+
}
|
|
2302
|
+
}));
|
|
2303
|
+
const res = await this.batchesClient().create({ requests });
|
|
2304
|
+
return res.id;
|
|
2305
|
+
}
|
|
2306
|
+
async translationBatchStatus(batchId) {
|
|
2307
|
+
const r = await this.batchesClient().retrieve(batchId);
|
|
2308
|
+
return { status: r.processing_status, counts: r.request_counts };
|
|
2309
|
+
}
|
|
2310
|
+
async translationBatchResults(batchId) {
|
|
2311
|
+
const out = /* @__PURE__ */ new Map();
|
|
2312
|
+
for await (const entry of await this.batchesClient().results(batchId)) {
|
|
2313
|
+
if (entry.result.type !== "succeeded") {
|
|
2314
|
+
out.set(entry.custom_id, { type: "failed", error: entry.result.error?.message ?? entry.result.type });
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2317
|
+
const text = entry.result.message?.content.find((b) => b.type === "text")?.text ?? "";
|
|
2318
|
+
try {
|
|
2319
|
+
out.set(entry.custom_id, { type: "items", items: parseReplyItems(text) });
|
|
2320
|
+
} catch (err) {
|
|
2321
|
+
if (!(err instanceof MalformedReplyError)) throw err;
|
|
2322
|
+
out.set(entry.custom_id, { type: "malformed", raw: err.raw });
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
return out;
|
|
2326
|
+
}
|
|
2327
|
+
async cancelTranslationBatch(batchId) {
|
|
2328
|
+
await this.batchesClient().cancel(batchId);
|
|
2329
|
+
}
|
|
2280
2330
|
async callBatch(batch, signal) {
|
|
2281
2331
|
const content = this.buildUserContent(batch);
|
|
2282
2332
|
const res = await this.client.messages.create({
|
|
@@ -3050,6 +3100,143 @@ var init_run = __esm({
|
|
|
3050
3100
|
}
|
|
3051
3101
|
});
|
|
3052
3102
|
|
|
3103
|
+
// src/server/ai/pending-batch.ts
|
|
3104
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3, rmSync as rmSync4 } from "fs";
|
|
3105
|
+
import { join as join3 } from "path";
|
|
3106
|
+
function pendingBatchPath(projectRoot) {
|
|
3107
|
+
return join3(projectRoot, ".glotfile", "batch.json");
|
|
3108
|
+
}
|
|
3109
|
+
function loadPendingBatch(projectRoot) {
|
|
3110
|
+
const path = pendingBatchPath(projectRoot);
|
|
3111
|
+
if (!existsSync6(path)) return void 0;
|
|
3112
|
+
try {
|
|
3113
|
+
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
3114
|
+
if (parsed?.version !== 1) return void 0;
|
|
3115
|
+
return parsed;
|
|
3116
|
+
} catch {
|
|
3117
|
+
return void 0;
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
function savePendingBatch(projectRoot, pending) {
|
|
3121
|
+
const dir = join3(projectRoot, ".glotfile");
|
|
3122
|
+
mkdirSync4(dir, { recursive: true });
|
|
3123
|
+
const gitignore = join3(dir, ".gitignore");
|
|
3124
|
+
if (!existsSync6(gitignore)) writeFileSync3(gitignore, "*\n");
|
|
3125
|
+
writeFileSync3(pendingBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
|
|
3126
|
+
}
|
|
3127
|
+
function clearPendingBatch(projectRoot) {
|
|
3128
|
+
rmSync4(pendingBatchPath(projectRoot), { force: true });
|
|
3129
|
+
}
|
|
3130
|
+
var init_pending_batch = __esm({
|
|
3131
|
+
"src/server/ai/pending-batch.ts"() {
|
|
3132
|
+
"use strict";
|
|
3133
|
+
}
|
|
3134
|
+
});
|
|
3135
|
+
|
|
3136
|
+
// src/server/ai/batch-run.ts
|
|
3137
|
+
function buildBatchJobs(reqs, batchSize) {
|
|
3138
|
+
const byLocale = /* @__PURE__ */ new Map();
|
|
3139
|
+
for (const req of reqs) {
|
|
3140
|
+
let group = byLocale.get(req.targetLocale);
|
|
3141
|
+
if (!group) {
|
|
3142
|
+
group = [];
|
|
3143
|
+
byLocale.set(req.targetLocale, group);
|
|
3144
|
+
}
|
|
3145
|
+
group.push(req);
|
|
3146
|
+
}
|
|
3147
|
+
const jobs = [];
|
|
3148
|
+
for (const [locale, group] of byLocale) {
|
|
3149
|
+
chunk(group, Math.max(1, batchSize)).forEach((batch, i) => {
|
|
3150
|
+
const safeLocale = locale.replace(/[^a-zA-Z0-9-]/g, "-").slice(0, 56);
|
|
3151
|
+
jobs.push({ customId: `${safeLocale}_${i}`, locale, requests: batch });
|
|
3152
|
+
});
|
|
3153
|
+
}
|
|
3154
|
+
return jobs;
|
|
3155
|
+
}
|
|
3156
|
+
async function submitBatchTranslation(state, provider, reqs, batchSize, model, projectRoot) {
|
|
3157
|
+
if (loadPendingBatch(projectRoot)) {
|
|
3158
|
+
throw new Error("A translation batch is already pending. Apply or cancel it first (`glotfile batch`).");
|
|
3159
|
+
}
|
|
3160
|
+
const jobs = buildBatchJobs(reqs, batchSize);
|
|
3161
|
+
const batchId = await provider.submitTranslationBatch(jobs);
|
|
3162
|
+
const pending = {
|
|
3163
|
+
version: 1,
|
|
3164
|
+
// Only Anthropic implements batch translation today.
|
|
3165
|
+
provider: "anthropic",
|
|
3166
|
+
model,
|
|
3167
|
+
batchId,
|
|
3168
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3169
|
+
total: reqs.length,
|
|
3170
|
+
jobs: jobs.map((j) => ({
|
|
3171
|
+
customId: j.customId,
|
|
3172
|
+
locale: j.locale,
|
|
3173
|
+
requests: j.requests.map((r) => {
|
|
3174
|
+
const { image: _image, ...rest } = r;
|
|
3175
|
+
return { ...rest, sourceHash: sourceHash(state.keys[r.key], state.config.sourceLocale) };
|
|
3176
|
+
})
|
|
3177
|
+
}))
|
|
3178
|
+
};
|
|
3179
|
+
savePendingBatch(projectRoot, pending);
|
|
3180
|
+
return pending;
|
|
3181
|
+
}
|
|
3182
|
+
async function applyBatchResults(load, persist, provider, pending, projectRoot, ai) {
|
|
3183
|
+
const outcomes = await provider.translationBatchResults(pending.batchId);
|
|
3184
|
+
const fresh = load();
|
|
3185
|
+
const isStale = (r) => {
|
|
3186
|
+
const entry = fresh.keys[r.key];
|
|
3187
|
+
return !entry || sourceHash(entry, fresh.config.sourceLocale) !== r.sourceHash;
|
|
3188
|
+
};
|
|
3189
|
+
const applied = [];
|
|
3190
|
+
const results = [];
|
|
3191
|
+
const retryReqs = [];
|
|
3192
|
+
let staleSkipped = 0;
|
|
3193
|
+
for (const job of pending.jobs) {
|
|
3194
|
+
const outcome = outcomes.get(job.customId);
|
|
3195
|
+
const itemsById = outcome?.type === "items" ? new Map(outcome.items.map((i) => [i.id, i])) : null;
|
|
3196
|
+
for (const stored of job.requests) {
|
|
3197
|
+
if (isStale(stored)) {
|
|
3198
|
+
staleSkipped++;
|
|
3199
|
+
continue;
|
|
3200
|
+
}
|
|
3201
|
+
const { sourceHash: _hash, ...req } = stored;
|
|
3202
|
+
if (!itemsById) {
|
|
3203
|
+
retryReqs.push(req);
|
|
3204
|
+
continue;
|
|
3205
|
+
}
|
|
3206
|
+
applied.push(req);
|
|
3207
|
+
results.push(validateReply(req, itemsById.get(req.id)));
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
let screenshotsSkipped = 0;
|
|
3211
|
+
if (retryReqs.length) {
|
|
3212
|
+
const { skipped } = attachScreenshotsForProvider(retryReqs, fresh, projectRoot, provider.supportsVision());
|
|
3213
|
+
screenshotsSkipped = skipped;
|
|
3214
|
+
const retryResults = await runLocaleParallel(
|
|
3215
|
+
retryReqs,
|
|
3216
|
+
provider,
|
|
3217
|
+
{},
|
|
3218
|
+
ai.concurrency,
|
|
3219
|
+
void 0,
|
|
3220
|
+
ai.batchSize
|
|
3221
|
+
);
|
|
3222
|
+
applied.push(...retryReqs);
|
|
3223
|
+
results.push(...retryResults);
|
|
3224
|
+
}
|
|
3225
|
+
const { written, errors } = applyResults(fresh, applied, results);
|
|
3226
|
+
persist(fresh);
|
|
3227
|
+
clearPendingBatch(projectRoot);
|
|
3228
|
+
return { written, errors, staleSkipped, retried: retryReqs.length, screenshotsSkipped };
|
|
3229
|
+
}
|
|
3230
|
+
var init_batch_run = __esm({
|
|
3231
|
+
"src/server/ai/batch-run.ts"() {
|
|
3232
|
+
"use strict";
|
|
3233
|
+
init_suppress();
|
|
3234
|
+
init_batch();
|
|
3235
|
+
init_run();
|
|
3236
|
+
init_pending_batch();
|
|
3237
|
+
}
|
|
3238
|
+
});
|
|
3239
|
+
|
|
3053
3240
|
// src/server/ai/pricing.ts
|
|
3054
3241
|
function bareModelId(model) {
|
|
3055
3242
|
let id = model.trim().toLowerCase();
|
|
@@ -3163,7 +3350,7 @@ var init_estimate = __esm({
|
|
|
3163
3350
|
});
|
|
3164
3351
|
|
|
3165
3352
|
// src/server/log.ts
|
|
3166
|
-
import { appendFileSync, readFileSync as
|
|
3353
|
+
import { appendFileSync, readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
3167
3354
|
import { resolve as resolve5 } from "path";
|
|
3168
3355
|
function logPath(projectRoot) {
|
|
3169
3356
|
return resolve5(projectRoot, ".glotfile", "log.jsonl");
|
|
@@ -3174,8 +3361,8 @@ function appendLog(projectRoot, entry) {
|
|
|
3174
3361
|
}
|
|
3175
3362
|
function readLog(projectRoot, limit = 100) {
|
|
3176
3363
|
const path = logPath(projectRoot);
|
|
3177
|
-
if (!
|
|
3178
|
-
const lines =
|
|
3364
|
+
if (!existsSync7(path)) return [];
|
|
3365
|
+
const lines = readFileSync7(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
3179
3366
|
const entries = lines.map((l) => JSON.parse(l));
|
|
3180
3367
|
return entries.reverse().slice(0, limit);
|
|
3181
3368
|
}
|
|
@@ -3187,13 +3374,13 @@ var init_log = __esm({
|
|
|
3187
3374
|
});
|
|
3188
3375
|
|
|
3189
3376
|
// src/server/scan.ts
|
|
3190
|
-
import { existsSync as
|
|
3377
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
3191
3378
|
import { resolve as resolve6 } from "path";
|
|
3192
3379
|
function loadUsageCache(projectRoot) {
|
|
3193
3380
|
const path = resolve6(projectRoot, ".glotfile", "usage.json");
|
|
3194
|
-
if (!
|
|
3381
|
+
if (!existsSync8(path)) return null;
|
|
3195
3382
|
try {
|
|
3196
|
-
return JSON.parse(
|
|
3383
|
+
return JSON.parse(readFileSync8(path, "utf8"));
|
|
3197
3384
|
} catch {
|
|
3198
3385
|
return null;
|
|
3199
3386
|
}
|
|
@@ -3258,8 +3445,8 @@ var init_scan = __esm({
|
|
|
3258
3445
|
});
|
|
3259
3446
|
|
|
3260
3447
|
// src/server/scanner.ts
|
|
3261
|
-
import { readdirSync as readdirSync3, statSync as statSync2, readFileSync as
|
|
3262
|
-
import { join as
|
|
3448
|
+
import { readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync9 } from "fs";
|
|
3449
|
+
import { join as join4, extname as extname2, relative } from "path";
|
|
3263
3450
|
function scannerForExt(ext) {
|
|
3264
3451
|
return EXT_SCANNER[ext] ?? null;
|
|
3265
3452
|
}
|
|
@@ -3411,7 +3598,7 @@ function* walkFiles(dir, root, exclude) {
|
|
|
3411
3598
|
}
|
|
3412
3599
|
for (const name of entries) {
|
|
3413
3600
|
if (ALWAYS_EXCLUDE.has(name)) continue;
|
|
3414
|
-
const abs =
|
|
3601
|
+
const abs = join4(dir, name);
|
|
3415
3602
|
const rel = relative(root, abs);
|
|
3416
3603
|
let st;
|
|
3417
3604
|
try {
|
|
@@ -3441,7 +3628,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
3441
3628
|
const ext = extname2(relPath);
|
|
3442
3629
|
const scanner = scannerForExt(ext);
|
|
3443
3630
|
if (!scanner) continue;
|
|
3444
|
-
const abs =
|
|
3631
|
+
const abs = join4(projectRoot, relPath);
|
|
3445
3632
|
let st;
|
|
3446
3633
|
try {
|
|
3447
3634
|
st = statSync2(abs);
|
|
@@ -3457,7 +3644,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
3457
3644
|
}
|
|
3458
3645
|
let content;
|
|
3459
3646
|
try {
|
|
3460
|
-
content =
|
|
3647
|
+
content = readFileSync9(abs, "utf8");
|
|
3461
3648
|
} catch {
|
|
3462
3649
|
continue;
|
|
3463
3650
|
}
|
|
@@ -3573,7 +3760,7 @@ var init_scanner = __esm({
|
|
|
3573
3760
|
});
|
|
3574
3761
|
|
|
3575
3762
|
// src/server/ai/context.ts
|
|
3576
|
-
import { existsSync as
|
|
3763
|
+
import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
|
|
3577
3764
|
import { resolve as resolve7 } from "path";
|
|
3578
3765
|
function globToRegExp2(glob) {
|
|
3579
3766
|
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
@@ -3588,8 +3775,8 @@ function extractSnippets(refs, projectRoot, fileCache) {
|
|
|
3588
3775
|
for (const ref of selected) {
|
|
3589
3776
|
const absPath = resolve7(projectRoot, ref.file);
|
|
3590
3777
|
if (!fileCache.has(ref.file)) {
|
|
3591
|
-
if (!
|
|
3592
|
-
const content =
|
|
3778
|
+
if (!existsSync9(absPath)) continue;
|
|
3779
|
+
const content = readFileSync10(absPath, "utf8");
|
|
3593
3780
|
fileCache.set(ref.file, content.split("\n"));
|
|
3594
3781
|
}
|
|
3595
3782
|
const lines = fileCache.get(ref.file);
|
|
@@ -4105,7 +4292,7 @@ var init_run2 = __esm({
|
|
|
4105
4292
|
});
|
|
4106
4293
|
|
|
4107
4294
|
// src/server/lint/outputs.ts
|
|
4108
|
-
import { readFileSync as
|
|
4295
|
+
import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
|
|
4109
4296
|
import { resolve as resolve8 } from "path";
|
|
4110
4297
|
function checkOutputs(state, root) {
|
|
4111
4298
|
const out = [];
|
|
@@ -4113,7 +4300,7 @@ function checkOutputs(state, root) {
|
|
|
4113
4300
|
const result = getAdapter(output.adapter).export(state, output);
|
|
4114
4301
|
for (const file of result.files) {
|
|
4115
4302
|
const abs = resolve8(root, file.path);
|
|
4116
|
-
const current =
|
|
4303
|
+
const current = existsSync10(abs) ? readFileSync11(abs, "utf8") : null;
|
|
4117
4304
|
if (current === null) {
|
|
4118
4305
|
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
4119
4306
|
} else if (current !== file.contents) {
|
|
@@ -4158,8 +4345,8 @@ var init_accept = __esm({
|
|
|
4158
4345
|
});
|
|
4159
4346
|
|
|
4160
4347
|
// src/server/import/detect.ts
|
|
4161
|
-
import { existsSync as
|
|
4162
|
-
import { join as
|
|
4348
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
4349
|
+
import { join as join5 } from "path";
|
|
4163
4350
|
function safeIsDir(p) {
|
|
4164
4351
|
try {
|
|
4165
4352
|
return statSync3(p).isDirectory();
|
|
@@ -4168,7 +4355,7 @@ function safeIsDir(p) {
|
|
|
4168
4355
|
}
|
|
4169
4356
|
}
|
|
4170
4357
|
function listDirs(dir) {
|
|
4171
|
-
return readdirSync4(dir).filter((e) => safeIsDir(
|
|
4358
|
+
return readdirSync4(dir).filter((e) => safeIsDir(join5(dir, e)));
|
|
4172
4359
|
}
|
|
4173
4360
|
function fileCount(dir) {
|
|
4174
4361
|
try {
|
|
@@ -4182,23 +4369,23 @@ function pickSource(locales, sizeOf) {
|
|
|
4182
4369
|
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
4183
4370
|
}
|
|
4184
4371
|
function detectLaravel(root) {
|
|
4185
|
-
const localeRoot = [
|
|
4372
|
+
const localeRoot = [join5(root, "resources", "lang"), join5(root, "lang")].find(safeIsDir);
|
|
4186
4373
|
if (!localeRoot) return null;
|
|
4187
4374
|
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
4188
4375
|
if (locales.length === 0) return null;
|
|
4189
|
-
const sourceLocale = pickSource(locales, (loc) => fileCount(
|
|
4376
|
+
const sourceLocale = pickSource(locales, (loc) => fileCount(join5(localeRoot, loc)));
|
|
4190
4377
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
4191
4378
|
}
|
|
4192
4379
|
function detectVue(root, forced = false) {
|
|
4193
4380
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
4194
|
-
const localeRoot =
|
|
4381
|
+
const localeRoot = join5(root, rel);
|
|
4195
4382
|
if (!safeIsDir(localeRoot)) continue;
|
|
4196
4383
|
const locales = readdirSync4(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4197
4384
|
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
4198
4385
|
if (enough) {
|
|
4199
4386
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4200
4387
|
try {
|
|
4201
|
-
return statSync3(
|
|
4388
|
+
return statSync3(join5(localeRoot, `${loc}.json`)).size;
|
|
4202
4389
|
} catch {
|
|
4203
4390
|
return 0;
|
|
4204
4391
|
}
|
|
@@ -4210,7 +4397,7 @@ function detectVue(root, forced = false) {
|
|
|
4210
4397
|
}
|
|
4211
4398
|
function detectArb(root) {
|
|
4212
4399
|
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
4213
|
-
const localeRoot =
|
|
4400
|
+
const localeRoot = join5(root, rel);
|
|
4214
4401
|
if (!safeIsDir(localeRoot)) continue;
|
|
4215
4402
|
const locales = readdirSync4(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
4216
4403
|
if (locales.length >= 1) {
|
|
@@ -4220,10 +4407,10 @@ function detectArb(root) {
|
|
|
4220
4407
|
return null;
|
|
4221
4408
|
}
|
|
4222
4409
|
function lprojLocales(dir) {
|
|
4223
|
-
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) &&
|
|
4410
|
+
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join5(dir, `${l}.lproj`, "Localizable.strings")));
|
|
4224
4411
|
}
|
|
4225
4412
|
function detectApple(root) {
|
|
4226
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
4413
|
+
const candidates = [root, ...listDirs(root).map((d) => join5(root, d))];
|
|
4227
4414
|
let best = null;
|
|
4228
4415
|
for (const dir of candidates) {
|
|
4229
4416
|
const locales = lprojLocales(dir);
|
|
@@ -4235,7 +4422,7 @@ function detectApple(root) {
|
|
|
4235
4422
|
locales,
|
|
4236
4423
|
sourceLocale: pickSource(locales, (loc) => {
|
|
4237
4424
|
try {
|
|
4238
|
-
return statSync3(
|
|
4425
|
+
return statSync3(join5(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
4239
4426
|
} catch {
|
|
4240
4427
|
return 0;
|
|
4241
4428
|
}
|
|
@@ -4247,7 +4434,7 @@ function detectApple(root) {
|
|
|
4247
4434
|
}
|
|
4248
4435
|
function detectAngularXliff(root) {
|
|
4249
4436
|
for (const rel of ANGULAR_DIR_CANDIDATES) {
|
|
4250
|
-
const localeRoot = rel === "." ? root :
|
|
4437
|
+
const localeRoot = rel === "." ? root : join5(root, rel);
|
|
4251
4438
|
if (!safeIsDir(localeRoot)) continue;
|
|
4252
4439
|
const files = readdirSync4(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
|
|
4253
4440
|
if (files.length === 0) continue;
|
|
@@ -4255,7 +4442,7 @@ function detectAngularXliff(root) {
|
|
|
4255
4442
|
const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
|
|
4256
4443
|
let sourceLocale;
|
|
4257
4444
|
try {
|
|
4258
|
-
sourceLocale =
|
|
4445
|
+
sourceLocale = readFileSync12(join5(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
|
|
4259
4446
|
} catch {
|
|
4260
4447
|
}
|
|
4261
4448
|
if (!sourceLocale && locales.length === 0) continue;
|
|
@@ -4266,14 +4453,14 @@ function detectAngularXliff(root) {
|
|
|
4266
4453
|
return null;
|
|
4267
4454
|
}
|
|
4268
4455
|
function detectRails(root) {
|
|
4269
|
-
const localeRoot =
|
|
4456
|
+
const localeRoot = join5(root, "config", "locales");
|
|
4270
4457
|
if (!safeIsDir(localeRoot)) return null;
|
|
4271
4458
|
const locales = [];
|
|
4272
4459
|
for (const file of readdirSync4(localeRoot).sort()) {
|
|
4273
4460
|
if (!/\.ya?ml$/.test(file)) continue;
|
|
4274
4461
|
let text;
|
|
4275
4462
|
try {
|
|
4276
|
-
text =
|
|
4463
|
+
text = readFileSync12(join5(localeRoot, file), "utf8");
|
|
4277
4464
|
} catch {
|
|
4278
4465
|
continue;
|
|
4279
4466
|
}
|
|
@@ -4287,15 +4474,15 @@ function detectRails(root) {
|
|
|
4287
4474
|
}
|
|
4288
4475
|
function detectI18next(root) {
|
|
4289
4476
|
for (const rel of I18NEXT_DIR_CANDIDATES) {
|
|
4290
|
-
const localeRoot =
|
|
4477
|
+
const localeRoot = join5(root, rel);
|
|
4291
4478
|
if (!safeIsDir(localeRoot)) continue;
|
|
4292
4479
|
const locales = listDirs(localeRoot).filter(
|
|
4293
|
-
(d) => LOCALE_RE.test(d) && readdirSync4(
|
|
4480
|
+
(d) => LOCALE_RE.test(d) && readdirSync4(join5(localeRoot, d)).some((f) => f.endsWith(".json"))
|
|
4294
4481
|
);
|
|
4295
4482
|
if (locales.length === 0) continue;
|
|
4296
4483
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4297
4484
|
try {
|
|
4298
|
-
return readdirSync4(
|
|
4485
|
+
return readdirSync4(join5(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync3(join5(localeRoot, loc, f)).size, 0);
|
|
4299
4486
|
} catch {
|
|
4300
4487
|
return 0;
|
|
4301
4488
|
}
|
|
@@ -4312,8 +4499,8 @@ function gettextLocales(dir) {
|
|
|
4312
4499
|
if (!locales.includes(flat)) locales.push(flat);
|
|
4313
4500
|
continue;
|
|
4314
4501
|
}
|
|
4315
|
-
if (!LOCALE_RE.test(entry) || !safeIsDir(
|
|
4316
|
-
const sub =
|
|
4502
|
+
if (!LOCALE_RE.test(entry) || !safeIsDir(join5(dir, entry))) continue;
|
|
4503
|
+
const sub = join5(dir, entry);
|
|
4317
4504
|
const hasPo = (d) => {
|
|
4318
4505
|
try {
|
|
4319
4506
|
return readdirSync4(d).some((f) => f.endsWith(".po"));
|
|
@@ -4321,7 +4508,7 @@ function gettextLocales(dir) {
|
|
|
4321
4508
|
return false;
|
|
4322
4509
|
}
|
|
4323
4510
|
};
|
|
4324
|
-
if (hasPo(
|
|
4511
|
+
if (hasPo(join5(sub, "LC_MESSAGES")) || hasPo(sub)) {
|
|
4325
4512
|
if (!locales.includes(entry)) locales.push(entry);
|
|
4326
4513
|
}
|
|
4327
4514
|
}
|
|
@@ -4329,7 +4516,7 @@ function gettextLocales(dir) {
|
|
|
4329
4516
|
}
|
|
4330
4517
|
function detectGettext(root) {
|
|
4331
4518
|
for (const rel of GETTEXT_DIR_CANDIDATES) {
|
|
4332
|
-
const localeRoot =
|
|
4519
|
+
const localeRoot = join5(root, rel);
|
|
4333
4520
|
if (!safeIsDir(localeRoot)) continue;
|
|
4334
4521
|
const locales = gettextLocales(localeRoot);
|
|
4335
4522
|
if (locales.length === 0) continue;
|
|
@@ -4338,10 +4525,10 @@ function detectGettext(root) {
|
|
|
4338
4525
|
return null;
|
|
4339
4526
|
}
|
|
4340
4527
|
function detectAppleStringsdict(root) {
|
|
4341
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
4528
|
+
const candidates = [root, ...listDirs(root).map((d) => join5(root, d))];
|
|
4342
4529
|
let best = null;
|
|
4343
4530
|
for (const dir of candidates) {
|
|
4344
|
-
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) &&
|
|
4531
|
+
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join5(dir, `${l}.lproj`, "Localizable.stringsdict")));
|
|
4345
4532
|
if (locales.length === 0) continue;
|
|
4346
4533
|
if (!best || locales.length > best.locales.length) {
|
|
4347
4534
|
best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
@@ -4350,7 +4537,7 @@ function detectAppleStringsdict(root) {
|
|
|
4350
4537
|
return best;
|
|
4351
4538
|
}
|
|
4352
4539
|
function detect(root, formatOverride) {
|
|
4353
|
-
if (!
|
|
4540
|
+
if (!existsSync11(root)) return null;
|
|
4354
4541
|
if (formatOverride) {
|
|
4355
4542
|
const fn = BY_FORMAT[formatOverride];
|
|
4356
4543
|
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
@@ -4424,8 +4611,8 @@ var init_flatten = __esm({
|
|
|
4424
4611
|
});
|
|
4425
4612
|
|
|
4426
4613
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
4427
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
4428
|
-
import { join as
|
|
4614
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync13 } from "fs";
|
|
4615
|
+
import { join as join6 } from "path";
|
|
4429
4616
|
var LOCALE_RE2, vueI18nJson2;
|
|
4430
4617
|
var init_vue_i18n_json2 = __esm({
|
|
4431
4618
|
"src/server/import/parsers/vue-i18n-json.ts"() {
|
|
@@ -4445,7 +4632,7 @@ var init_vue_i18n_json2 = __esm({
|
|
|
4445
4632
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4446
4633
|
let data;
|
|
4447
4634
|
try {
|
|
4448
|
-
data = JSON.parse(
|
|
4635
|
+
data = JSON.parse(readFileSync13(join6(localeRoot, file), "utf8"));
|
|
4449
4636
|
} catch (e) {
|
|
4450
4637
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
4451
4638
|
continue;
|
|
@@ -4473,16 +4660,16 @@ var init_placeholders2 = __esm({
|
|
|
4473
4660
|
|
|
4474
4661
|
// src/server/import/parsers/laravel-php.ts
|
|
4475
4662
|
import { readdirSync as readdirSync6, statSync as statSync4 } from "fs";
|
|
4476
|
-
import { join as
|
|
4663
|
+
import { join as join7, relative as relative2 } from "path";
|
|
4477
4664
|
import { execFileSync } from "child_process";
|
|
4478
4665
|
function listDirs2(dir) {
|
|
4479
|
-
return readdirSync6(dir).filter((e) => statSync4(
|
|
4666
|
+
return readdirSync6(dir).filter((e) => statSync4(join7(dir, e)).isDirectory());
|
|
4480
4667
|
}
|
|
4481
4668
|
function listPhpFiles(dir) {
|
|
4482
4669
|
const out = [];
|
|
4483
4670
|
const walk = (d) => {
|
|
4484
4671
|
for (const e of readdirSync6(d)) {
|
|
4485
|
-
const full =
|
|
4672
|
+
const full = join7(d, e);
|
|
4486
4673
|
if (statSync4(full).isDirectory()) walk(full);
|
|
4487
4674
|
else if (e.endsWith(".php")) out.push(full);
|
|
4488
4675
|
}
|
|
@@ -4525,7 +4712,7 @@ var init_laravel_php2 = __esm({
|
|
|
4525
4712
|
for (const locale of listDirs2(localeRoot).sort()) {
|
|
4526
4713
|
if (locale === "vendor") continue;
|
|
4527
4714
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4528
|
-
const localeDir =
|
|
4715
|
+
const localeDir = join7(localeRoot, locale);
|
|
4529
4716
|
locales.push(locale);
|
|
4530
4717
|
for (const file of listPhpFiles(localeDir)) {
|
|
4531
4718
|
const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
|
|
@@ -4550,8 +4737,8 @@ var init_laravel_php2 = __esm({
|
|
|
4550
4737
|
});
|
|
4551
4738
|
|
|
4552
4739
|
// src/server/import/parsers/flutter-arb.ts
|
|
4553
|
-
import { readdirSync as readdirSync7, readFileSync as
|
|
4554
|
-
import { join as
|
|
4740
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync14 } from "fs";
|
|
4741
|
+
import { join as join8 } from "path";
|
|
4555
4742
|
function localeFromArbName(file) {
|
|
4556
4743
|
const m = file.match(/^(.+)\.arb$/);
|
|
4557
4744
|
if (!m) return null;
|
|
@@ -4591,7 +4778,7 @@ var init_flutter_arb2 = __esm({
|
|
|
4591
4778
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4592
4779
|
let data;
|
|
4593
4780
|
try {
|
|
4594
|
-
data = JSON.parse(
|
|
4781
|
+
data = JSON.parse(readFileSync14(join8(localeRoot, file), "utf8"));
|
|
4595
4782
|
} catch (e) {
|
|
4596
4783
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
4597
4784
|
continue;
|
|
@@ -4618,8 +4805,8 @@ var init_flutter_arb2 = __esm({
|
|
|
4618
4805
|
});
|
|
4619
4806
|
|
|
4620
4807
|
// src/server/import/parsers/apple-strings.ts
|
|
4621
|
-
import { readdirSync as readdirSync8, readFileSync as
|
|
4622
|
-
import { join as
|
|
4808
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync15, statSync as statSync5 } from "fs";
|
|
4809
|
+
import { join as join9 } from "path";
|
|
4623
4810
|
function localeFromLproj(dir) {
|
|
4624
4811
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
4625
4812
|
if (!m) return null;
|
|
@@ -4727,16 +4914,16 @@ var init_apple_strings2 = __esm({
|
|
|
4727
4914
|
const locale = localeFromLproj(dir);
|
|
4728
4915
|
if (!locale) continue;
|
|
4729
4916
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4730
|
-
const file =
|
|
4917
|
+
const file = join9(localeRoot, dir, TABLE);
|
|
4731
4918
|
let text;
|
|
4732
4919
|
try {
|
|
4733
4920
|
if (!statSync5(file).isFile()) continue;
|
|
4734
|
-
text =
|
|
4921
|
+
text = readFileSync15(file, "utf8");
|
|
4735
4922
|
} catch {
|
|
4736
4923
|
continue;
|
|
4737
4924
|
}
|
|
4738
4925
|
locales.push(locale);
|
|
4739
|
-
const others = readdirSync8(
|
|
4926
|
+
const others = readdirSync8(join9(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
|
|
4740
4927
|
if (others.length) {
|
|
4741
4928
|
warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
|
|
4742
4929
|
}
|
|
@@ -4751,8 +4938,8 @@ var init_apple_strings2 = __esm({
|
|
|
4751
4938
|
});
|
|
4752
4939
|
|
|
4753
4940
|
// src/server/import/parsers/angular-xliff.ts
|
|
4754
|
-
import { readdirSync as readdirSync9, readFileSync as
|
|
4755
|
-
import { join as
|
|
4941
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync16 } from "fs";
|
|
4942
|
+
import { join as join10 } from "path";
|
|
4756
4943
|
function decodeEntities(s) {
|
|
4757
4944
|
return s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))).replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
4758
4945
|
}
|
|
@@ -4804,7 +4991,7 @@ var init_angular_xliff2 = __esm({
|
|
|
4804
4991
|
if (fnameLocale !== void 0 && !LOCALE_RE5.test(fnameLocale)) continue;
|
|
4805
4992
|
let xml;
|
|
4806
4993
|
try {
|
|
4807
|
-
xml =
|
|
4994
|
+
xml = readFileSync16(join10(localeRoot, file), "utf8");
|
|
4808
4995
|
} catch (e) {
|
|
4809
4996
|
warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
|
|
4810
4997
|
continue;
|
|
@@ -4847,8 +5034,8 @@ var init_angular_xliff2 = __esm({
|
|
|
4847
5034
|
});
|
|
4848
5035
|
|
|
4849
5036
|
// src/server/import/parsers/gettext-po.ts
|
|
4850
|
-
import { readdirSync as readdirSync10, readFileSync as
|
|
4851
|
-
import { join as
|
|
5037
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync17 } from "fs";
|
|
5038
|
+
import { join as join11 } from "path";
|
|
4852
5039
|
function unescapePo(s) {
|
|
4853
5040
|
return s.replace(
|
|
4854
5041
|
/\\([\\"ntr])/g,
|
|
@@ -4919,17 +5106,17 @@ function discoverPoFiles(root) {
|
|
|
4919
5106
|
for (const e of entries) {
|
|
4920
5107
|
if (e.isFile() && e.name.endsWith(".po")) {
|
|
4921
5108
|
const base = e.name.slice(0, -3);
|
|
4922
|
-
found.push({ path:
|
|
5109
|
+
found.push({ path: join11(root, e.name), rel: e.name, locale: LOCALE_RE6.test(base) ? base : null });
|
|
4923
5110
|
} else if (e.isDirectory() && LOCALE_RE6.test(e.name)) {
|
|
4924
|
-
for (const sub of [
|
|
5111
|
+
for (const sub of [join11(e.name, "LC_MESSAGES"), e.name]) {
|
|
4925
5112
|
let names;
|
|
4926
5113
|
try {
|
|
4927
|
-
names = readdirSync10(
|
|
5114
|
+
names = readdirSync10(join11(root, sub)).sort();
|
|
4928
5115
|
} catch {
|
|
4929
5116
|
continue;
|
|
4930
5117
|
}
|
|
4931
5118
|
for (const f of names) {
|
|
4932
|
-
if (f.endsWith(".po")) found.push({ path:
|
|
5119
|
+
if (f.endsWith(".po")) found.push({ path: join11(root, sub, f), rel: join11(sub, f), locale: e.name });
|
|
4933
5120
|
}
|
|
4934
5121
|
}
|
|
4935
5122
|
}
|
|
@@ -4953,7 +5140,7 @@ var init_gettext_po2 = __esm({
|
|
|
4953
5140
|
for (const file of discoverPoFiles(localeRoot)) {
|
|
4954
5141
|
let entries;
|
|
4955
5142
|
try {
|
|
4956
|
-
entries = parseEntries(
|
|
5143
|
+
entries = parseEntries(readFileSync17(file.path, "utf8"));
|
|
4957
5144
|
} catch (e) {
|
|
4958
5145
|
warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
|
|
4959
5146
|
continue;
|
|
@@ -5000,8 +5187,8 @@ var init_gettext_po2 = __esm({
|
|
|
5000
5187
|
});
|
|
5001
5188
|
|
|
5002
5189
|
// src/server/import/parsers/i18next-json.ts
|
|
5003
|
-
import { readdirSync as readdirSync11, readFileSync as
|
|
5004
|
-
import { join as
|
|
5190
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync18, statSync as statSync6 } from "fs";
|
|
5191
|
+
import { join as join12 } from "path";
|
|
5005
5192
|
function safeIsDir2(p) {
|
|
5006
5193
|
try {
|
|
5007
5194
|
return statSync6(p).isDirectory();
|
|
@@ -5016,7 +5203,7 @@ function fromI18next(value) {
|
|
|
5016
5203
|
function ingestFile(path, label, prefix, locale, keys, warnings) {
|
|
5017
5204
|
let data;
|
|
5018
5205
|
try {
|
|
5019
|
-
data = JSON.parse(
|
|
5206
|
+
data = JSON.parse(readFileSync18(path, "utf8"));
|
|
5020
5207
|
} catch (e) {
|
|
5021
5208
|
warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
|
|
5022
5209
|
return false;
|
|
@@ -5069,7 +5256,7 @@ var init_i18next_json2 = __esm({
|
|
|
5069
5256
|
const keys = {};
|
|
5070
5257
|
const locales = [];
|
|
5071
5258
|
for (const entry of readdirSync11(localeRoot).sort()) {
|
|
5072
|
-
const full =
|
|
5259
|
+
const full = join12(localeRoot, entry);
|
|
5073
5260
|
if (safeIsDir2(full)) {
|
|
5074
5261
|
if (!LOCALE_RE7.test(entry)) continue;
|
|
5075
5262
|
if (opts?.locales && !opts.locales.includes(entry)) continue;
|
|
@@ -5078,7 +5265,7 @@ var init_i18next_json2 = __esm({
|
|
|
5078
5265
|
if (!file.endsWith(".json")) continue;
|
|
5079
5266
|
const ns = file.slice(0, -".json".length);
|
|
5080
5267
|
const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
|
|
5081
|
-
if (ingestFile(
|
|
5268
|
+
if (ingestFile(join12(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
|
|
5082
5269
|
}
|
|
5083
5270
|
if (any && !locales.includes(entry)) locales.push(entry);
|
|
5084
5271
|
} else if (entry.endsWith(".json")) {
|
|
@@ -5097,8 +5284,8 @@ var init_i18next_json2 = __esm({
|
|
|
5097
5284
|
});
|
|
5098
5285
|
|
|
5099
5286
|
// src/server/import/parsers/rails-yaml.ts
|
|
5100
|
-
import { readdirSync as readdirSync12, readFileSync as
|
|
5101
|
-
import { join as
|
|
5287
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync19 } from "fs";
|
|
5288
|
+
import { join as join13 } from "path";
|
|
5102
5289
|
function fromRuby(value) {
|
|
5103
5290
|
return value.replace(/%\{(\w+)\}/g, "{$1}");
|
|
5104
5291
|
}
|
|
@@ -5315,7 +5502,7 @@ var init_rails_yaml2 = __esm({
|
|
|
5315
5502
|
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
5316
5503
|
let text;
|
|
5317
5504
|
try {
|
|
5318
|
-
text =
|
|
5505
|
+
text = readFileSync19(join13(localeRoot, file), "utf8");
|
|
5319
5506
|
} catch (e) {
|
|
5320
5507
|
warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
|
|
5321
5508
|
continue;
|
|
@@ -5338,8 +5525,8 @@ var init_rails_yaml2 = __esm({
|
|
|
5338
5525
|
});
|
|
5339
5526
|
|
|
5340
5527
|
// src/server/import/parsers/apple-stringsdict.ts
|
|
5341
|
-
import { readdirSync as readdirSync13, readFileSync as
|
|
5342
|
-
import { join as
|
|
5528
|
+
import { readdirSync as readdirSync13, readFileSync as readFileSync20, statSync as statSync7 } from "fs";
|
|
5529
|
+
import { join as join14 } from "path";
|
|
5343
5530
|
function localeFromLproj2(dir) {
|
|
5344
5531
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
5345
5532
|
if (!m) return null;
|
|
@@ -5481,16 +5668,16 @@ var init_apple_stringsdict2 = __esm({
|
|
|
5481
5668
|
const locale = localeFromLproj2(dir);
|
|
5482
5669
|
if (!locale) continue;
|
|
5483
5670
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5484
|
-
const file =
|
|
5671
|
+
const file = join14(localeRoot, dir, TABLE2);
|
|
5485
5672
|
let text;
|
|
5486
5673
|
try {
|
|
5487
5674
|
if (!statSync7(file).isFile()) continue;
|
|
5488
|
-
text =
|
|
5675
|
+
text = readFileSync20(file, "utf8");
|
|
5489
5676
|
} catch {
|
|
5490
5677
|
continue;
|
|
5491
5678
|
}
|
|
5492
5679
|
locales.push(locale);
|
|
5493
|
-
const others = readdirSync13(
|
|
5680
|
+
const others = readdirSync13(join14(localeRoot, dir)).filter(
|
|
5494
5681
|
(f) => f.endsWith(".stringsdict") && f !== TABLE2
|
|
5495
5682
|
);
|
|
5496
5683
|
if (others.length) {
|
|
@@ -5949,12 +6136,12 @@ var init_checks = __esm({
|
|
|
5949
6136
|
});
|
|
5950
6137
|
|
|
5951
6138
|
// src/server/ui-prefs.ts
|
|
5952
|
-
import { readFileSync as
|
|
6139
|
+
import { readFileSync as readFileSync21 } from "fs";
|
|
5953
6140
|
import { homedir } from "os";
|
|
5954
|
-
import { join as
|
|
6141
|
+
import { join as join15 } from "path";
|
|
5955
6142
|
function readJson2(path) {
|
|
5956
6143
|
try {
|
|
5957
|
-
const parsed = JSON.parse(
|
|
6144
|
+
const parsed = JSON.parse(readFileSync21(path, "utf8"));
|
|
5958
6145
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
5959
6146
|
} catch {
|
|
5960
6147
|
return {};
|
|
@@ -5979,7 +6166,7 @@ var init_ui_prefs = __esm({
|
|
|
5979
6166
|
THEMES = ["system", "light", "dark"];
|
|
5980
6167
|
isThemeMode = (v) => THEMES.includes(v);
|
|
5981
6168
|
isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
|
|
5982
|
-
defaultUiPrefsPath = () =>
|
|
6169
|
+
defaultUiPrefsPath = () => join15(homedir(), ".glotfile", "ui.json");
|
|
5983
6170
|
DEFAULTS = { theme: "system" };
|
|
5984
6171
|
}
|
|
5985
6172
|
});
|
|
@@ -5987,13 +6174,13 @@ var init_ui_prefs = __esm({
|
|
|
5987
6174
|
// src/server/api.ts
|
|
5988
6175
|
import { Hono } from "hono";
|
|
5989
6176
|
import { streamSSE } from "hono/streaming";
|
|
5990
|
-
import { readFileSync as
|
|
6177
|
+
import { readFileSync as readFileSync22, existsSync as existsSync12, readdirSync as readdirSync14, statSync as statSync8, rmSync as rmSync5 } from "fs";
|
|
5991
6178
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
5992
6179
|
function projectName(root) {
|
|
5993
6180
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
5994
|
-
if (
|
|
6181
|
+
if (existsSync12(nameFile)) {
|
|
5995
6182
|
try {
|
|
5996
|
-
const name =
|
|
6183
|
+
const name = readFileSync22(nameFile, "utf8").trim();
|
|
5997
6184
|
if (name) return name;
|
|
5998
6185
|
} catch {
|
|
5999
6186
|
}
|
|
@@ -6126,7 +6313,7 @@ function createApi(deps) {
|
|
|
6126
6313
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
6127
6314
|
const abs = resolve9(dir, name);
|
|
6128
6315
|
let filePath = null;
|
|
6129
|
-
if ((name === "glotfile" || name.endsWith(".glotfile")) &&
|
|
6316
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync12(resolve9(abs, "config.json"))) {
|
|
6130
6317
|
filePath = resolve9(dir, `${name}.json`);
|
|
6131
6318
|
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
6132
6319
|
filePath = abs;
|
|
@@ -6160,7 +6347,7 @@ function createApi(deps) {
|
|
|
6160
6347
|
const resolved = resolve9(projectRoot, path);
|
|
6161
6348
|
const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
|
|
6162
6349
|
if (!inside) return c.json({ error: "file is outside the project" }, 400);
|
|
6163
|
-
if (!
|
|
6350
|
+
if (!existsSync12(resolved)) return c.json({ error: "file not found" }, 400);
|
|
6164
6351
|
loadState(resolved);
|
|
6165
6352
|
deps.statePath = resolved;
|
|
6166
6353
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
@@ -6221,9 +6408,9 @@ function createApi(deps) {
|
|
|
6221
6408
|
const abs = resolve9(root, screenshot);
|
|
6222
6409
|
const rel = relative4(root, abs);
|
|
6223
6410
|
const seg0 = rel.split(sep2)[0] ?? "";
|
|
6224
|
-
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") &&
|
|
6411
|
+
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync12(abs)) {
|
|
6225
6412
|
try {
|
|
6226
|
-
|
|
6413
|
+
rmSync5(abs);
|
|
6227
6414
|
} catch {
|
|
6228
6415
|
}
|
|
6229
6416
|
}
|
|
@@ -6761,6 +6948,104 @@ function createApi(deps) {
|
|
|
6761
6948
|
const ai = loadLocalSettings(projectRoot).ai;
|
|
6762
6949
|
return c.json(estimateTranslation(load(), ai, { onlyMissing: body.onlyMissing ?? true, keys, locales }));
|
|
6763
6950
|
});
|
|
6951
|
+
app.get("/batch/status", async (c) => {
|
|
6952
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
6953
|
+
let supported = false;
|
|
6954
|
+
let provider;
|
|
6955
|
+
try {
|
|
6956
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
6957
|
+
supported = supportsBatchTranslate(provider);
|
|
6958
|
+
} catch {
|
|
6959
|
+
}
|
|
6960
|
+
const pending = loadPendingBatch(projectRoot);
|
|
6961
|
+
if (!pending) return c.json({ supported, pending: null });
|
|
6962
|
+
const base = { batchId: pending.batchId, createdAt: pending.createdAt, model: pending.model, total: pending.total };
|
|
6963
|
+
if (!provider || !supportsBatchTranslate(provider)) {
|
|
6964
|
+
return c.json({ supported, pending: { ...base, status: "unknown", counts: null } });
|
|
6965
|
+
}
|
|
6966
|
+
try {
|
|
6967
|
+
const status = await provider.translationBatchStatus(pending.batchId);
|
|
6968
|
+
return c.json({ supported, pending: { ...base, status: status.status, counts: status.counts } });
|
|
6969
|
+
} catch (e) {
|
|
6970
|
+
return c.json({ supported, pending: { ...base, status: "unknown", counts: null, error: e.message } });
|
|
6971
|
+
}
|
|
6972
|
+
});
|
|
6973
|
+
app.post("/batch/translate", (c) => withTranslateLock(async () => {
|
|
6974
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6975
|
+
const s = load();
|
|
6976
|
+
const reqs = selectRequests(s, {
|
|
6977
|
+
onlyMissing: body.onlyMissing ?? true,
|
|
6978
|
+
keys: Array.isArray(body.keys) && body.keys.length ? body.keys.filter(Boolean) : void 0,
|
|
6979
|
+
locales: Array.isArray(body.locales) && body.locales.length ? body.locales.filter(Boolean) : void 0
|
|
6980
|
+
});
|
|
6981
|
+
if (!reqs.length) return c.json({ error: "Nothing to translate." }, 400);
|
|
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 (!supportsBatchTranslate(provider)) {
|
|
6990
|
+
return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
|
|
6991
|
+
}
|
|
6992
|
+
attachScreenshotsForProvider(reqs, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
6993
|
+
let pending;
|
|
6994
|
+
try {
|
|
6995
|
+
pending = await submitBatchTranslation(s, provider, reqs, aiCfg.batchSize, aiCfg.model, projectRoot);
|
|
6996
|
+
} catch (e) {
|
|
6997
|
+
return c.json({ error: e.message }, 409);
|
|
6998
|
+
}
|
|
6999
|
+
appendLog(projectRoot, {
|
|
7000
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7001
|
+
kind: "translate",
|
|
7002
|
+
summary: `Submitted batch ${pending.batchId} (${pending.total} items)`,
|
|
7003
|
+
model: aiCfg.model,
|
|
7004
|
+
system: buildSystemPrompt(reqs.some((r) => r.plural !== void 0)),
|
|
7005
|
+
items: reqs.map((r) => ({ id: r.id, key: r.key, source: r.source, targetLocale: r.targetLocale, context: r.context, glossary: r.glossary, screenshot: s.keys[r.key]?.screenshot }))
|
|
7006
|
+
});
|
|
7007
|
+
console.log(`[batch] submitted ${pending.batchId} \u2014 ${pending.total} string(s)`);
|
|
7008
|
+
return c.json({ batchId: pending.batchId, total: pending.total });
|
|
7009
|
+
}));
|
|
7010
|
+
app.post("/batch/apply", (c) => withTranslateLock(async () => {
|
|
7011
|
+
const pending = loadPendingBatch(projectRoot);
|
|
7012
|
+
if (!pending) return c.json({ error: "No pending batch." }, 404);
|
|
7013
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7014
|
+
let provider;
|
|
7015
|
+
try {
|
|
7016
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7017
|
+
} catch (e) {
|
|
7018
|
+
return c.json({ error: e.message }, 400);
|
|
7019
|
+
}
|
|
7020
|
+
if (!supportsBatchTranslate(provider)) {
|
|
7021
|
+
return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
|
|
7022
|
+
}
|
|
7023
|
+
const outcome = await applyBatchResults(load, persist, provider, pending, projectRoot, {
|
|
7024
|
+
batchSize: aiCfg.batchSize,
|
|
7025
|
+
concurrency: aiCfg.concurrency
|
|
7026
|
+
});
|
|
7027
|
+
appendLog(projectRoot, {
|
|
7028
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7029
|
+
kind: "translate",
|
|
7030
|
+
summary: `Applied batch ${pending.batchId}: wrote ${outcome.written}, ${outcome.retried} retried, ${outcome.staleSkipped} stale`,
|
|
7031
|
+
model: aiCfg.model,
|
|
7032
|
+
results: []
|
|
7033
|
+
});
|
|
7034
|
+
console.log(`[batch] applied ${pending.batchId} \u2014 wrote ${outcome.written}, ${outcome.errors.length} error(s)`);
|
|
7035
|
+
return c.json(outcome);
|
|
7036
|
+
}));
|
|
7037
|
+
app.post("/batch/cancel", async (c) => {
|
|
7038
|
+
const pending = loadPendingBatch(projectRoot);
|
|
7039
|
+
if (!pending) return c.json({ error: "No pending batch." }, 404);
|
|
7040
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7041
|
+
try {
|
|
7042
|
+
const provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7043
|
+
if (supportsBatchTranslate(provider)) await provider.cancelTranslationBatch(pending.batchId);
|
|
7044
|
+
} catch {
|
|
7045
|
+
}
|
|
7046
|
+
clearPendingBatch(projectRoot);
|
|
7047
|
+
return c.json({ canceled: pending.batchId });
|
|
7048
|
+
});
|
|
6764
7049
|
app.get("/log", (c) => c.json(readLog(projectRoot, 100)));
|
|
6765
7050
|
app.post("/scan", async (c) => {
|
|
6766
7051
|
const s = load();
|
|
@@ -6941,6 +7226,8 @@ var init_api = __esm({
|
|
|
6941
7226
|
init_ai();
|
|
6942
7227
|
init_run();
|
|
6943
7228
|
init_provider();
|
|
7229
|
+
init_batch_run();
|
|
7230
|
+
init_pending_batch();
|
|
6944
7231
|
init_estimate();
|
|
6945
7232
|
init_log();
|
|
6946
7233
|
init_schema();
|
|
@@ -6963,7 +7250,7 @@ __export(server_exports, {
|
|
|
6963
7250
|
import { Hono as Hono2 } from "hono";
|
|
6964
7251
|
import { serve } from "@hono/node-server";
|
|
6965
7252
|
import { fileURLToPath } from "url";
|
|
6966
|
-
import { dirname as dirname4, join as
|
|
7253
|
+
import { dirname as dirname4, join as join16, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
6967
7254
|
import { readFile, stat } from "fs/promises";
|
|
6968
7255
|
import { createServer } from "net";
|
|
6969
7256
|
import open from "open";
|
|
@@ -7006,7 +7293,7 @@ function buildApp(opts) {
|
|
|
7006
7293
|
const file = await readFileResponse(target);
|
|
7007
7294
|
if (file) return file;
|
|
7008
7295
|
}
|
|
7009
|
-
const index = await readFileResponse(
|
|
7296
|
+
const index = await readFileResponse(join16(root, "index.html"));
|
|
7010
7297
|
if (index) return index;
|
|
7011
7298
|
return c.notFound();
|
|
7012
7299
|
});
|
|
@@ -7064,7 +7351,7 @@ var init_server = __esm({
|
|
|
7064
7351
|
init_scan();
|
|
7065
7352
|
init_scanner();
|
|
7066
7353
|
here = dirname4(fileURLToPath(import.meta.url));
|
|
7067
|
-
DEFAULT_UI_DIR =
|
|
7354
|
+
DEFAULT_UI_DIR = join16(here, "..", "ui");
|
|
7068
7355
|
MIME = {
|
|
7069
7356
|
".html": "text/html; charset=utf-8",
|
|
7070
7357
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -7101,6 +7388,8 @@ init_ai();
|
|
|
7101
7388
|
init_local_settings();
|
|
7102
7389
|
init_run();
|
|
7103
7390
|
init_provider();
|
|
7391
|
+
init_batch_run();
|
|
7392
|
+
init_pending_batch();
|
|
7104
7393
|
init_estimate();
|
|
7105
7394
|
init_log();
|
|
7106
7395
|
init_scan();
|
|
@@ -7108,8 +7397,8 @@ init_scanner();
|
|
|
7108
7397
|
init_context();
|
|
7109
7398
|
init_run2();
|
|
7110
7399
|
init_outputs();
|
|
7111
|
-
import { resolve as resolve11, dirname as dirname5, join as
|
|
7112
|
-
import { readFileSync as
|
|
7400
|
+
import { resolve as resolve11, dirname as dirname5, join as join17 } from "path";
|
|
7401
|
+
import { readFileSync as readFileSync23, existsSync as existsSync13, mkdirSync as mkdirSync5, cpSync } from "fs";
|
|
7113
7402
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7114
7403
|
|
|
7115
7404
|
// src/server/lint/locate.ts
|
|
@@ -7176,7 +7465,7 @@ function formatSarif(report, rawText) {
|
|
|
7176
7465
|
}
|
|
7177
7466
|
|
|
7178
7467
|
// src/server/cli.ts
|
|
7179
|
-
var COMMANDS = ["serve", "export", "translate", "lint", "check", "import", "build-context", "scan", "prune", "split", "skill"];
|
|
7468
|
+
var COMMANDS = ["serve", "export", "translate", "lint", "check", "import", "build-context", "scan", "prune", "split", "skill", "batch"];
|
|
7180
7469
|
var isCommand = (s) => s != null && COMMANDS.includes(s);
|
|
7181
7470
|
function parseArgs(argv) {
|
|
7182
7471
|
const statePath = resolve11(process.cwd(), "glotfile.json");
|
|
@@ -7246,7 +7535,12 @@ function parseArgs(argv) {
|
|
|
7246
7535
|
else if (flag === "--unused") args.unused = true;
|
|
7247
7536
|
else if (flag === "--write") args.write = true;
|
|
7248
7537
|
else if (flag === "--estimate") args.estimate = true;
|
|
7538
|
+
else if (flag === "--batch") args.batch = true;
|
|
7539
|
+
else if (flag === "--wait") args.wait = true;
|
|
7249
7540
|
else if (flag === "--print") args.print = true;
|
|
7541
|
+
else if (args.command === "batch" && (flag === "status" || flag === "apply" || flag === "cancel")) {
|
|
7542
|
+
args.batchAction = flag;
|
|
7543
|
+
}
|
|
7250
7544
|
}
|
|
7251
7545
|
return args;
|
|
7252
7546
|
}
|
|
@@ -7297,6 +7591,15 @@ async function runExport(args) {
|
|
|
7297
7591
|
await new Promise(() => {
|
|
7298
7592
|
});
|
|
7299
7593
|
}
|
|
7594
|
+
function makeProviderOrExit(ai) {
|
|
7595
|
+
try {
|
|
7596
|
+
return makeProvider(ai);
|
|
7597
|
+
} catch (e) {
|
|
7598
|
+
console.error(e.message);
|
|
7599
|
+
process.exitCode = 1;
|
|
7600
|
+
return null;
|
|
7601
|
+
}
|
|
7602
|
+
}
|
|
7300
7603
|
async function runTranslate(args) {
|
|
7301
7604
|
const state = loadState(args.statePath);
|
|
7302
7605
|
const projectRoot = dirname5(resolve11(args.statePath));
|
|
@@ -7333,18 +7636,51 @@ async function runTranslate(args) {
|
|
|
7333
7636
|
keyGlob: args.keyGlob
|
|
7334
7637
|
});
|
|
7335
7638
|
const toTranslate = [...reqs];
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7639
|
+
if (args.batch) {
|
|
7640
|
+
if (!toTranslate.length) {
|
|
7641
|
+
console.log("Nothing to translate.");
|
|
7642
|
+
return;
|
|
7643
|
+
}
|
|
7339
7644
|
const ai = loadLocalSettings(projectRoot).ai;
|
|
7340
|
-
|
|
7645
|
+
const provider = makeProviderOrExit(ai);
|
|
7646
|
+
if (!provider) return;
|
|
7647
|
+
if (!supportsBatchTranslate(provider)) {
|
|
7648
|
+
console.error(`Provider "${ai.provider}" does not support batch mode. Currently anthropic only.`);
|
|
7649
|
+
process.exitCode = 1;
|
|
7650
|
+
return;
|
|
7651
|
+
}
|
|
7652
|
+
const { skipped } = attachScreenshotsForProvider(toTranslate, state, projectRoot, provider.supportsVision());
|
|
7653
|
+
if (skipped) console.warn(`Model "${ai.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
7654
|
+
let pending;
|
|
7341
7655
|
try {
|
|
7342
|
-
|
|
7656
|
+
pending = await submitBatchTranslation(state, provider, toTranslate, ai.batchSize, ai.model, projectRoot);
|
|
7343
7657
|
} catch (e) {
|
|
7344
7658
|
console.error(e.message);
|
|
7345
7659
|
process.exitCode = 1;
|
|
7346
7660
|
return;
|
|
7347
7661
|
}
|
|
7662
|
+
appendLog(projectRoot, {
|
|
7663
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7664
|
+
kind: "translate",
|
|
7665
|
+
summary: `Submitted batch ${pending.batchId} (${pending.total} items)`,
|
|
7666
|
+
model: ai.model,
|
|
7667
|
+
system: buildSystemPrompt(toTranslate.some((r) => r.plural !== void 0)),
|
|
7668
|
+
items: toTranslate.map((r) => ({ id: r.id, key: r.key, source: r.source, targetLocale: r.targetLocale, context: r.context, glossary: r.glossary, screenshot: state.keys[r.key]?.screenshot }))
|
|
7669
|
+
});
|
|
7670
|
+
console.log(`Submitted batch ${pending.batchId} \u2014 ${pending.total} string(s) at 50% batch pricing.`);
|
|
7671
|
+
if (!args.wait) {
|
|
7672
|
+
console.log("Check progress with `glotfile batch`; it applies results automatically when finished.");
|
|
7673
|
+
return;
|
|
7674
|
+
}
|
|
7675
|
+
await waitAndApply(args, provider, pending, ai);
|
|
7676
|
+
return;
|
|
7677
|
+
}
|
|
7678
|
+
let written = 0;
|
|
7679
|
+
let errors = [];
|
|
7680
|
+
if (toTranslate.length) {
|
|
7681
|
+
const ai = loadLocalSettings(projectRoot).ai;
|
|
7682
|
+
const provider = makeProviderOrExit(ai);
|
|
7683
|
+
if (!provider) return;
|
|
7348
7684
|
const { skipped } = attachScreenshotsForProvider(toTranslate, state, projectRoot, provider.supportsVision());
|
|
7349
7685
|
if (skipped) console.warn(`Model "${ai.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
7350
7686
|
console.log(`Translating ${toTranslate.length} string(s)\u2026`);
|
|
@@ -7401,6 +7737,80 @@ async function runTranslate(args) {
|
|
|
7401
7737
|
console.log(`Wrote ${written} machine translation(s).`);
|
|
7402
7738
|
for (const e of errors) console.warn(`skip ${e.key} @ ${e.locale}: ${e.error}`);
|
|
7403
7739
|
}
|
|
7740
|
+
function reportApply(outcome) {
|
|
7741
|
+
console.log(`Wrote ${outcome.written} machine translation(s).`);
|
|
7742
|
+
if (outcome.retried) console.log(`${outcome.retried} item(s) re-run synchronously (batch entries failed or were malformed).`);
|
|
7743
|
+
if (outcome.staleSkipped) console.log(`${outcome.staleSkipped} result(s) skipped \u2014 source changed since submission.`);
|
|
7744
|
+
if (outcome.screenshotsSkipped) console.log(`${outcome.screenshotsSkipped} screenshot(s) ignored on retry (model has no vision support).`);
|
|
7745
|
+
for (const e of outcome.errors) console.warn(`skip ${e.key} @ ${e.locale}: ${e.error}`);
|
|
7746
|
+
}
|
|
7747
|
+
async function applyPending(args, provider, pending, ai) {
|
|
7748
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
7749
|
+
const outcome = await applyBatchResults(
|
|
7750
|
+
() => loadState(args.statePath),
|
|
7751
|
+
(s) => saveState(args.statePath, s),
|
|
7752
|
+
provider,
|
|
7753
|
+
pending,
|
|
7754
|
+
projectRoot,
|
|
7755
|
+
{ batchSize: ai.batchSize, concurrency: ai.concurrency }
|
|
7756
|
+
);
|
|
7757
|
+
reportApply(outcome);
|
|
7758
|
+
}
|
|
7759
|
+
async function waitAndApply(args, provider, pending, ai) {
|
|
7760
|
+
for (; ; ) {
|
|
7761
|
+
const status = await provider.translationBatchStatus(pending.batchId);
|
|
7762
|
+
const c = status.counts;
|
|
7763
|
+
process.stdout.write(`\r ${c.succeeded + c.errored + c.expired + c.canceled}/${pending.jobs.length} entries done (${c.processing} processing)`);
|
|
7764
|
+
if (status.status === "ended") break;
|
|
7765
|
+
await new Promise((r) => setTimeout(r, 6e4));
|
|
7766
|
+
}
|
|
7767
|
+
process.stdout.write("\n");
|
|
7768
|
+
await applyPending(args, provider, pending, ai);
|
|
7769
|
+
}
|
|
7770
|
+
async function runBatch(args) {
|
|
7771
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
7772
|
+
const pending = loadPendingBatch(projectRoot);
|
|
7773
|
+
if (!pending) {
|
|
7774
|
+
console.log("No pending batch. Start one with `glotfile translate --batch`.");
|
|
7775
|
+
return;
|
|
7776
|
+
}
|
|
7777
|
+
const action = args.batchAction ?? "status";
|
|
7778
|
+
if (action === "cancel") {
|
|
7779
|
+
let remoteFailed = false;
|
|
7780
|
+
try {
|
|
7781
|
+
const ai2 = loadLocalSettings(projectRoot).ai;
|
|
7782
|
+
const provider2 = makeProvider(ai2);
|
|
7783
|
+
if (supportsBatchTranslate(provider2)) {
|
|
7784
|
+
await provider2.cancelTranslationBatch(pending.batchId);
|
|
7785
|
+
} else {
|
|
7786
|
+
remoteFailed = true;
|
|
7787
|
+
}
|
|
7788
|
+
} catch {
|
|
7789
|
+
remoteFailed = true;
|
|
7790
|
+
}
|
|
7791
|
+
clearPendingBatch(projectRoot);
|
|
7792
|
+
const suffix = remoteFailed ? " (remote cancel failed \u2014 it will expire server-side)" : "";
|
|
7793
|
+
console.log(`Canceled batch ${pending.batchId}.${suffix}`);
|
|
7794
|
+
return;
|
|
7795
|
+
}
|
|
7796
|
+
const ai = loadLocalSettings(projectRoot).ai;
|
|
7797
|
+
const provider = makeProviderOrExit(ai);
|
|
7798
|
+
if (!provider) return;
|
|
7799
|
+
if (!supportsBatchTranslate(provider)) {
|
|
7800
|
+
console.error(`Pending batch was submitted via anthropic, but the configured provider "${ai.provider}" has no batch support.`);
|
|
7801
|
+
process.exitCode = 1;
|
|
7802
|
+
return;
|
|
7803
|
+
}
|
|
7804
|
+
const status = await provider.translationBatchStatus(pending.batchId);
|
|
7805
|
+
const c = status.counts;
|
|
7806
|
+
console.log(`Batch ${pending.batchId} (${pending.total} string(s), submitted ${pending.createdAt})`);
|
|
7807
|
+
console.log(` ${status.status} \u2014 ${c.succeeded} succeeded, ${c.processing} processing, ${c.errored} errored, ${c.expired} expired, ${c.canceled} canceled`);
|
|
7808
|
+
if (status.status !== "ended") {
|
|
7809
|
+
if (action === "apply") console.log("Not finished yet \u2014 try again later.");
|
|
7810
|
+
return;
|
|
7811
|
+
}
|
|
7812
|
+
await applyPending(args, provider, pending, ai);
|
|
7813
|
+
}
|
|
7404
7814
|
function printReport(report, format, rawText) {
|
|
7405
7815
|
if (format === "json") console.log(formatJson(report).trimEnd());
|
|
7406
7816
|
else if (format === "sarif") console.log(formatSarif(report, rawText).trimEnd());
|
|
@@ -7420,7 +7830,7 @@ async function runLintCmd(args) {
|
|
|
7420
7830
|
}
|
|
7421
7831
|
return;
|
|
7422
7832
|
}
|
|
7423
|
-
const rawText =
|
|
7833
|
+
const rawText = existsSync13(args.statePath) ? readFileSync23(args.statePath, "utf8") : "";
|
|
7424
7834
|
const report = await runLint(state, {
|
|
7425
7835
|
locales: args.locales,
|
|
7426
7836
|
ruleIds: args.ruleIds,
|
|
@@ -7444,7 +7854,7 @@ async function runCheck(args) {
|
|
|
7444
7854
|
process.exitCode = 1;
|
|
7445
7855
|
return;
|
|
7446
7856
|
}
|
|
7447
|
-
const rawText =
|
|
7857
|
+
const rawText = existsSync13(args.statePath) ? readFileSync23(args.statePath, "utf8") : "";
|
|
7448
7858
|
const root = dirname5(resolve11(args.statePath));
|
|
7449
7859
|
const lint = await runLint(state, {});
|
|
7450
7860
|
const findings = sortFindings([...lint.findings, ...checkOutputs(state, root)]);
|
|
@@ -7457,7 +7867,7 @@ async function runImportCmd(args) {
|
|
|
7457
7867
|
const { runImport: runImport2 } = await Promise.resolve().then(() => (init_run3(), run_exports));
|
|
7458
7868
|
const projectRoot = args.importSource ? resolve11(args.importSource) : dirname5(resolve11(args.statePath));
|
|
7459
7869
|
const out = resolve11(projectRoot, "glotfile.json");
|
|
7460
|
-
if (
|
|
7870
|
+
if (existsSync13(out) && !args.importForce) {
|
|
7461
7871
|
console.error(`${out} already exists; pass --force to overwrite`);
|
|
7462
7872
|
process.exitCode = 1;
|
|
7463
7873
|
return;
|
|
@@ -7605,19 +8015,19 @@ function runSplit(args) {
|
|
|
7605
8015
|
`Split catalog into ${splitDirFor(args.statePath)}/ (config.json, keys.json, locales/ \u2014 up to ${state.config.locales.length} locale files). Removed ${args.statePath}.`
|
|
7606
8016
|
);
|
|
7607
8017
|
}
|
|
7608
|
-
var SKILL_SRC =
|
|
8018
|
+
var SKILL_SRC = join17(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "skill");
|
|
7609
8019
|
function runSkill(args) {
|
|
7610
8020
|
if (args.print) {
|
|
7611
|
-
console.log(
|
|
8021
|
+
console.log(readFileSync23(join17(SKILL_SRC, "SKILL.md"), "utf8").trimEnd());
|
|
7612
8022
|
return;
|
|
7613
8023
|
}
|
|
7614
8024
|
const dest = resolve11(process.cwd(), ".claude", "skills", "glotfile");
|
|
7615
|
-
if (
|
|
8025
|
+
if (existsSync13(dest) && !args.importForce) {
|
|
7616
8026
|
console.error(`${dest} already exists; pass --force to overwrite`);
|
|
7617
8027
|
process.exitCode = 1;
|
|
7618
8028
|
return;
|
|
7619
8029
|
}
|
|
7620
|
-
|
|
8030
|
+
mkdirSync5(dirname5(dest), { recursive: true });
|
|
7621
8031
|
cpSync(SKILL_SRC, dest, { recursive: true });
|
|
7622
8032
|
console.log(`Installed the glotfile skill to ${dest}. Restart Claude Code to pick it up.`);
|
|
7623
8033
|
}
|
|
@@ -7646,7 +8056,9 @@ var COMMAND_HELP = {
|
|
|
7646
8056
|
["--all", "Re-translate every string, not just empty values"],
|
|
7647
8057
|
["--estimate", "Print batches, tokens and estimated cost without translating"],
|
|
7648
8058
|
["--locale <list>", "Comma-separated target locales (alias: --locales)"],
|
|
7649
|
-
["--key <glob>", "Only keys matching this glob"]
|
|
8059
|
+
["--key <glob>", "Only keys matching this glob"],
|
|
8060
|
+
["--batch", "Submit via the provider's batch API (50% cost, async; anthropic only)"],
|
|
8061
|
+
["--wait", "With --batch: stay attached, poll until finished, then apply"]
|
|
7650
8062
|
]
|
|
7651
8063
|
},
|
|
7652
8064
|
lint: {
|
|
@@ -7714,6 +8126,15 @@ var COMMAND_HELP = {
|
|
|
7714
8126
|
["--print", "Write SKILL.md to stdout instead of installing"],
|
|
7715
8127
|
["--force", "Overwrite an existing installed skill"]
|
|
7716
8128
|
]
|
|
8129
|
+
},
|
|
8130
|
+
batch: {
|
|
8131
|
+
summary: "Check, apply, or cancel a pending batch translation.",
|
|
8132
|
+
usage: "glotfile batch [status|apply|cancel]",
|
|
8133
|
+
options: [
|
|
8134
|
+
["status", "Show the pending batch's progress (default)"],
|
|
8135
|
+
["apply", "Fetch results and write translations (auto-runs when finished)"],
|
|
8136
|
+
["cancel", "Cancel the pending batch and discard the handle"]
|
|
8137
|
+
]
|
|
7717
8138
|
}
|
|
7718
8139
|
};
|
|
7719
8140
|
function formatOpts(opts) {
|
|
@@ -7746,8 +8167,8 @@ ${formatOpts([...options, ...GLOBAL_OPTS])}`);
|
|
|
7746
8167
|
);
|
|
7747
8168
|
}
|
|
7748
8169
|
function printVersion() {
|
|
7749
|
-
const pkgPath =
|
|
7750
|
-
console.log(JSON.parse(
|
|
8170
|
+
const pkgPath = join17(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
|
|
8171
|
+
console.log(JSON.parse(readFileSync23(pkgPath, "utf8")).version);
|
|
7751
8172
|
}
|
|
7752
8173
|
async function main(argv) {
|
|
7753
8174
|
const args = parseArgs(argv);
|
|
@@ -7770,6 +8191,7 @@ async function main(argv) {
|
|
|
7770
8191
|
if (args.command === "prune") return runPrune(args);
|
|
7771
8192
|
if (args.command === "split") return runSplit(args);
|
|
7772
8193
|
if (args.command === "skill") return runSkill(args);
|
|
8194
|
+
if (args.command === "batch") return runBatch(args);
|
|
7773
8195
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
7774
8196
|
const { url } = await startServer2({ statePath: args.statePath, dev: args.dev });
|
|
7775
8197
|
if (args.dev) console.log(`Glotfile dev API on ${url} \u2014 open the UI at the Vite "Local:" URL above`);
|