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