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/server.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Hono as Hono2 } from "hono";
|
|
3
3
|
import { serve } from "@hono/node-server";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
-
import { dirname as dirname4, join as
|
|
5
|
+
import { dirname as dirname4, join as join16, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
6
6
|
import { readFile, stat } from "fs/promises";
|
|
7
7
|
import { createServer } from "net";
|
|
8
8
|
import open from "open";
|
|
@@ -2841,13 +2841,16 @@ function checkOutputs(state, root) {
|
|
|
2841
2841
|
}
|
|
2842
2842
|
|
|
2843
2843
|
// src/server/api.ts
|
|
2844
|
-
import { readFileSync as
|
|
2844
|
+
import { readFileSync as readFileSync22, existsSync as existsSync12, readdirSync as readdirSync14, statSync as statSync8, rmSync as rmSync5 } from "fs";
|
|
2845
2845
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
2846
2846
|
|
|
2847
2847
|
// src/server/ai/anthropic.ts
|
|
2848
2848
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2849
2849
|
|
|
2850
2850
|
// src/server/ai/provider.ts
|
|
2851
|
+
function supportsBatchTranslate(p) {
|
|
2852
|
+
return typeof p.submitTranslationBatch === "function";
|
|
2853
|
+
}
|
|
2851
2854
|
function buildSystemPrompt(hasPluralItems) {
|
|
2852
2855
|
const lines = [
|
|
2853
2856
|
"You are a professional software localization engine for a UI string catalog.",
|
|
@@ -3136,6 +3139,53 @@ var AnthropicProvider = class {
|
|
|
3136
3139
|
return {};
|
|
3137
3140
|
}
|
|
3138
3141
|
}
|
|
3142
|
+
batchesClient() {
|
|
3143
|
+
const b = this.client.messages.batches;
|
|
3144
|
+
if (!b) throw new Error("Anthropic client has no batches support.");
|
|
3145
|
+
return b;
|
|
3146
|
+
}
|
|
3147
|
+
// Each job becomes one batch entry whose params mirror callBatch exactly —
|
|
3148
|
+
// same prompts, schema, and vision blocks — so batch and sync replies are
|
|
3149
|
+
// interchangeable downstream.
|
|
3150
|
+
async submitTranslationBatch(jobs) {
|
|
3151
|
+
const requests = jobs.map((job) => ({
|
|
3152
|
+
custom_id: job.customId,
|
|
3153
|
+
params: {
|
|
3154
|
+
model: this.config.model,
|
|
3155
|
+
max_tokens: 8192,
|
|
3156
|
+
// Batch entries don't share a live cache window, so cache_control is omitted here.
|
|
3157
|
+
system: [{ type: "text", text: buildSystemPrompt(job.requests.some((r) => r.plural !== void 0)) }],
|
|
3158
|
+
output_config: { format: { type: "json_schema", schema: BATCH_SCHEMA } },
|
|
3159
|
+
messages: [{ role: "user", content: this.buildUserContent(job.requests) }]
|
|
3160
|
+
}
|
|
3161
|
+
}));
|
|
3162
|
+
const res = await this.batchesClient().create({ requests });
|
|
3163
|
+
return res.id;
|
|
3164
|
+
}
|
|
3165
|
+
async translationBatchStatus(batchId) {
|
|
3166
|
+
const r = await this.batchesClient().retrieve(batchId);
|
|
3167
|
+
return { status: r.processing_status, counts: r.request_counts };
|
|
3168
|
+
}
|
|
3169
|
+
async translationBatchResults(batchId) {
|
|
3170
|
+
const out = /* @__PURE__ */ new Map();
|
|
3171
|
+
for await (const entry of await this.batchesClient().results(batchId)) {
|
|
3172
|
+
if (entry.result.type !== "succeeded") {
|
|
3173
|
+
out.set(entry.custom_id, { type: "failed", error: entry.result.error?.message ?? entry.result.type });
|
|
3174
|
+
continue;
|
|
3175
|
+
}
|
|
3176
|
+
const text = entry.result.message?.content.find((b) => b.type === "text")?.text ?? "";
|
|
3177
|
+
try {
|
|
3178
|
+
out.set(entry.custom_id, { type: "items", items: parseReplyItems(text) });
|
|
3179
|
+
} catch (err) {
|
|
3180
|
+
if (!(err instanceof MalformedReplyError)) throw err;
|
|
3181
|
+
out.set(entry.custom_id, { type: "malformed", raw: err.raw });
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
return out;
|
|
3185
|
+
}
|
|
3186
|
+
async cancelTranslationBatch(batchId) {
|
|
3187
|
+
await this.batchesClient().cancel(batchId);
|
|
3188
|
+
}
|
|
3139
3189
|
async callBatch(batch, signal) {
|
|
3140
3190
|
const content = this.buildUserContent(batch);
|
|
3141
3191
|
const res = await this.client.messages.create({
|
|
@@ -3682,6 +3732,129 @@ function applyResults(state, reqs, results, clock = systemClock, force = false)
|
|
|
3682
3732
|
return { written, errors };
|
|
3683
3733
|
}
|
|
3684
3734
|
|
|
3735
|
+
// src/server/ai/pending-batch.ts
|
|
3736
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync3, rmSync as rmSync4 } from "fs";
|
|
3737
|
+
import { join as join4 } from "path";
|
|
3738
|
+
function pendingBatchPath(projectRoot) {
|
|
3739
|
+
return join4(projectRoot, ".glotfile", "batch.json");
|
|
3740
|
+
}
|
|
3741
|
+
function loadPendingBatch(projectRoot) {
|
|
3742
|
+
const path = pendingBatchPath(projectRoot);
|
|
3743
|
+
if (!existsSync8(path)) return void 0;
|
|
3744
|
+
try {
|
|
3745
|
+
const parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
3746
|
+
if (parsed?.version !== 1) return void 0;
|
|
3747
|
+
return parsed;
|
|
3748
|
+
} catch {
|
|
3749
|
+
return void 0;
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
function savePendingBatch(projectRoot, pending) {
|
|
3753
|
+
const dir = join4(projectRoot, ".glotfile");
|
|
3754
|
+
mkdirSync4(dir, { recursive: true });
|
|
3755
|
+
const gitignore = join4(dir, ".gitignore");
|
|
3756
|
+
if (!existsSync8(gitignore)) writeFileSync3(gitignore, "*\n");
|
|
3757
|
+
writeFileSync3(pendingBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
|
|
3758
|
+
}
|
|
3759
|
+
function clearPendingBatch(projectRoot) {
|
|
3760
|
+
rmSync4(pendingBatchPath(projectRoot), { force: true });
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
// src/server/ai/batch-run.ts
|
|
3764
|
+
function buildBatchJobs(reqs, batchSize) {
|
|
3765
|
+
const byLocale = /* @__PURE__ */ new Map();
|
|
3766
|
+
for (const req of reqs) {
|
|
3767
|
+
let group = byLocale.get(req.targetLocale);
|
|
3768
|
+
if (!group) {
|
|
3769
|
+
group = [];
|
|
3770
|
+
byLocale.set(req.targetLocale, group);
|
|
3771
|
+
}
|
|
3772
|
+
group.push(req);
|
|
3773
|
+
}
|
|
3774
|
+
const jobs = [];
|
|
3775
|
+
for (const [locale, group] of byLocale) {
|
|
3776
|
+
chunk(group, Math.max(1, batchSize)).forEach((batch, i) => {
|
|
3777
|
+
const safeLocale = locale.replace(/[^a-zA-Z0-9-]/g, "-").slice(0, 56);
|
|
3778
|
+
jobs.push({ customId: `${safeLocale}_${i}`, locale, requests: batch });
|
|
3779
|
+
});
|
|
3780
|
+
}
|
|
3781
|
+
return jobs;
|
|
3782
|
+
}
|
|
3783
|
+
async function submitBatchTranslation(state, provider, reqs, batchSize, model, projectRoot) {
|
|
3784
|
+
if (loadPendingBatch(projectRoot)) {
|
|
3785
|
+
throw new Error("A translation batch is already pending. Apply or cancel it first (`glotfile batch`).");
|
|
3786
|
+
}
|
|
3787
|
+
const jobs = buildBatchJobs(reqs, batchSize);
|
|
3788
|
+
const batchId = await provider.submitTranslationBatch(jobs);
|
|
3789
|
+
const pending = {
|
|
3790
|
+
version: 1,
|
|
3791
|
+
// Only Anthropic implements batch translation today.
|
|
3792
|
+
provider: "anthropic",
|
|
3793
|
+
model,
|
|
3794
|
+
batchId,
|
|
3795
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3796
|
+
total: reqs.length,
|
|
3797
|
+
jobs: jobs.map((j) => ({
|
|
3798
|
+
customId: j.customId,
|
|
3799
|
+
locale: j.locale,
|
|
3800
|
+
requests: j.requests.map((r) => {
|
|
3801
|
+
const { image: _image, ...rest } = r;
|
|
3802
|
+
return { ...rest, sourceHash: sourceHash(state.keys[r.key], state.config.sourceLocale) };
|
|
3803
|
+
})
|
|
3804
|
+
}))
|
|
3805
|
+
};
|
|
3806
|
+
savePendingBatch(projectRoot, pending);
|
|
3807
|
+
return pending;
|
|
3808
|
+
}
|
|
3809
|
+
async function applyBatchResults(load, persist, provider, pending, projectRoot, ai) {
|
|
3810
|
+
const outcomes = await provider.translationBatchResults(pending.batchId);
|
|
3811
|
+
const fresh = load();
|
|
3812
|
+
const isStale = (r) => {
|
|
3813
|
+
const entry = fresh.keys[r.key];
|
|
3814
|
+
return !entry || sourceHash(entry, fresh.config.sourceLocale) !== r.sourceHash;
|
|
3815
|
+
};
|
|
3816
|
+
const applied = [];
|
|
3817
|
+
const results = [];
|
|
3818
|
+
const retryReqs = [];
|
|
3819
|
+
let staleSkipped = 0;
|
|
3820
|
+
for (const job of pending.jobs) {
|
|
3821
|
+
const outcome = outcomes.get(job.customId);
|
|
3822
|
+
const itemsById = outcome?.type === "items" ? new Map(outcome.items.map((i) => [i.id, i])) : null;
|
|
3823
|
+
for (const stored of job.requests) {
|
|
3824
|
+
if (isStale(stored)) {
|
|
3825
|
+
staleSkipped++;
|
|
3826
|
+
continue;
|
|
3827
|
+
}
|
|
3828
|
+
const { sourceHash: _hash, ...req } = stored;
|
|
3829
|
+
if (!itemsById) {
|
|
3830
|
+
retryReqs.push(req);
|
|
3831
|
+
continue;
|
|
3832
|
+
}
|
|
3833
|
+
applied.push(req);
|
|
3834
|
+
results.push(validateReply(req, itemsById.get(req.id)));
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
let screenshotsSkipped = 0;
|
|
3838
|
+
if (retryReqs.length) {
|
|
3839
|
+
const { skipped } = attachScreenshotsForProvider(retryReqs, fresh, projectRoot, provider.supportsVision());
|
|
3840
|
+
screenshotsSkipped = skipped;
|
|
3841
|
+
const retryResults = await runLocaleParallel(
|
|
3842
|
+
retryReqs,
|
|
3843
|
+
provider,
|
|
3844
|
+
{},
|
|
3845
|
+
ai.concurrency,
|
|
3846
|
+
void 0,
|
|
3847
|
+
ai.batchSize
|
|
3848
|
+
);
|
|
3849
|
+
applied.push(...retryReqs);
|
|
3850
|
+
results.push(...retryResults);
|
|
3851
|
+
}
|
|
3852
|
+
const { written, errors } = applyResults(fresh, applied, results);
|
|
3853
|
+
persist(fresh);
|
|
3854
|
+
clearPendingBatch(projectRoot);
|
|
3855
|
+
return { written, errors, staleSkipped, retried: retryReqs.length, screenshotsSkipped };
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3685
3858
|
// src/server/ai/pricing.ts
|
|
3686
3859
|
var PRICE_TABLE = [
|
|
3687
3860
|
["claude-fable-5", 10, 50],
|
|
@@ -3779,7 +3952,7 @@ function estimateTranslation(state, ai, opts) {
|
|
|
3779
3952
|
}
|
|
3780
3953
|
|
|
3781
3954
|
// src/server/log.ts
|
|
3782
|
-
import { appendFileSync, readFileSync as
|
|
3955
|
+
import { appendFileSync, readFileSync as readFileSync9, existsSync as existsSync9 } from "fs";
|
|
3783
3956
|
import { resolve as resolve6 } from "path";
|
|
3784
3957
|
function logPath(projectRoot) {
|
|
3785
3958
|
return resolve6(projectRoot, ".glotfile", "log.jsonl");
|
|
@@ -3790,8 +3963,8 @@ function appendLog(projectRoot, entry) {
|
|
|
3790
3963
|
}
|
|
3791
3964
|
function readLog(projectRoot, limit = 100) {
|
|
3792
3965
|
const path = logPath(projectRoot);
|
|
3793
|
-
if (!
|
|
3794
|
-
const lines =
|
|
3966
|
+
if (!existsSync9(path)) return [];
|
|
3967
|
+
const lines = readFileSync9(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
3795
3968
|
const entries = lines.map((l) => JSON.parse(l));
|
|
3796
3969
|
return entries.reverse().slice(0, limit);
|
|
3797
3970
|
}
|
|
@@ -3800,8 +3973,8 @@ function readLog(projectRoot, limit = 100) {
|
|
|
3800
3973
|
import { relative as relative3 } from "path";
|
|
3801
3974
|
|
|
3802
3975
|
// src/server/import/detect.ts
|
|
3803
|
-
import { existsSync as
|
|
3804
|
-
import { join as
|
|
3976
|
+
import { existsSync as existsSync10, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync2 } from "fs";
|
|
3977
|
+
import { join as join5 } from "path";
|
|
3805
3978
|
var LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
3806
3979
|
var VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
|
|
3807
3980
|
function safeIsDir(p) {
|
|
@@ -3812,7 +3985,7 @@ function safeIsDir(p) {
|
|
|
3812
3985
|
}
|
|
3813
3986
|
}
|
|
3814
3987
|
function listDirs(dir) {
|
|
3815
|
-
return readdirSync3(dir).filter((e) => safeIsDir(
|
|
3988
|
+
return readdirSync3(dir).filter((e) => safeIsDir(join5(dir, e)));
|
|
3816
3989
|
}
|
|
3817
3990
|
function fileCount(dir) {
|
|
3818
3991
|
try {
|
|
@@ -3826,23 +3999,23 @@ function pickSource(locales, sizeOf) {
|
|
|
3826
3999
|
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
3827
4000
|
}
|
|
3828
4001
|
function detectLaravel(root) {
|
|
3829
|
-
const localeRoot = [
|
|
4002
|
+
const localeRoot = [join5(root, "resources", "lang"), join5(root, "lang")].find(safeIsDir);
|
|
3830
4003
|
if (!localeRoot) return null;
|
|
3831
4004
|
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
3832
4005
|
if (locales.length === 0) return null;
|
|
3833
|
-
const sourceLocale = pickSource(locales, (loc) => fileCount(
|
|
4006
|
+
const sourceLocale = pickSource(locales, (loc) => fileCount(join5(localeRoot, loc)));
|
|
3834
4007
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
3835
4008
|
}
|
|
3836
4009
|
function detectVue(root, forced = false) {
|
|
3837
4010
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
3838
|
-
const localeRoot =
|
|
4011
|
+
const localeRoot = join5(root, rel);
|
|
3839
4012
|
if (!safeIsDir(localeRoot)) continue;
|
|
3840
4013
|
const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
3841
4014
|
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
3842
4015
|
if (enough) {
|
|
3843
4016
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
3844
4017
|
try {
|
|
3845
|
-
return statSync2(
|
|
4018
|
+
return statSync2(join5(localeRoot, `${loc}.json`)).size;
|
|
3846
4019
|
} catch {
|
|
3847
4020
|
return 0;
|
|
3848
4021
|
}
|
|
@@ -3854,7 +4027,7 @@ function detectVue(root, forced = false) {
|
|
|
3854
4027
|
}
|
|
3855
4028
|
function detectArb(root) {
|
|
3856
4029
|
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
3857
|
-
const localeRoot =
|
|
4030
|
+
const localeRoot = join5(root, rel);
|
|
3858
4031
|
if (!safeIsDir(localeRoot)) continue;
|
|
3859
4032
|
const locales = readdirSync3(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
3860
4033
|
if (locales.length >= 1) {
|
|
@@ -3864,10 +4037,10 @@ function detectArb(root) {
|
|
|
3864
4037
|
return null;
|
|
3865
4038
|
}
|
|
3866
4039
|
function lprojLocales(dir) {
|
|
3867
|
-
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) &&
|
|
4040
|
+
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync10(join5(dir, `${l}.lproj`, "Localizable.strings")));
|
|
3868
4041
|
}
|
|
3869
4042
|
function detectApple(root) {
|
|
3870
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
4043
|
+
const candidates = [root, ...listDirs(root).map((d) => join5(root, d))];
|
|
3871
4044
|
let best = null;
|
|
3872
4045
|
for (const dir of candidates) {
|
|
3873
4046
|
const locales = lprojLocales(dir);
|
|
@@ -3879,7 +4052,7 @@ function detectApple(root) {
|
|
|
3879
4052
|
locales,
|
|
3880
4053
|
sourceLocale: pickSource(locales, (loc) => {
|
|
3881
4054
|
try {
|
|
3882
|
-
return statSync2(
|
|
4055
|
+
return statSync2(join5(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
3883
4056
|
} catch {
|
|
3884
4057
|
return 0;
|
|
3885
4058
|
}
|
|
@@ -3892,7 +4065,7 @@ function detectApple(root) {
|
|
|
3892
4065
|
var ANGULAR_DIR_CANDIDATES = [".", "src/locale", "src/locales", "src/i18n", "locale", "locales", "i18n", "translations"];
|
|
3893
4066
|
function detectAngularXliff(root) {
|
|
3894
4067
|
for (const rel of ANGULAR_DIR_CANDIDATES) {
|
|
3895
|
-
const localeRoot = rel === "." ? root :
|
|
4068
|
+
const localeRoot = rel === "." ? root : join5(root, rel);
|
|
3896
4069
|
if (!safeIsDir(localeRoot)) continue;
|
|
3897
4070
|
const files = readdirSync3(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
|
|
3898
4071
|
if (files.length === 0) continue;
|
|
@@ -3900,7 +4073,7 @@ function detectAngularXliff(root) {
|
|
|
3900
4073
|
const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
|
|
3901
4074
|
let sourceLocale;
|
|
3902
4075
|
try {
|
|
3903
|
-
sourceLocale =
|
|
4076
|
+
sourceLocale = readFileSync10(join5(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
|
|
3904
4077
|
} catch {
|
|
3905
4078
|
}
|
|
3906
4079
|
if (!sourceLocale && locales.length === 0) continue;
|
|
@@ -3911,14 +4084,14 @@ function detectAngularXliff(root) {
|
|
|
3911
4084
|
return null;
|
|
3912
4085
|
}
|
|
3913
4086
|
function detectRails(root) {
|
|
3914
|
-
const localeRoot =
|
|
4087
|
+
const localeRoot = join5(root, "config", "locales");
|
|
3915
4088
|
if (!safeIsDir(localeRoot)) return null;
|
|
3916
4089
|
const locales = [];
|
|
3917
4090
|
for (const file of readdirSync3(localeRoot).sort()) {
|
|
3918
4091
|
if (!/\.ya?ml$/.test(file)) continue;
|
|
3919
4092
|
let text;
|
|
3920
4093
|
try {
|
|
3921
|
-
text =
|
|
4094
|
+
text = readFileSync10(join5(localeRoot, file), "utf8");
|
|
3922
4095
|
} catch {
|
|
3923
4096
|
continue;
|
|
3924
4097
|
}
|
|
@@ -3933,15 +4106,15 @@ function detectRails(root) {
|
|
|
3933
4106
|
var I18NEXT_DIR_CANDIDATES = ["public/locales", "static/locales", "locales", "src/locales", "src/i18n/locales"];
|
|
3934
4107
|
function detectI18next(root) {
|
|
3935
4108
|
for (const rel of I18NEXT_DIR_CANDIDATES) {
|
|
3936
|
-
const localeRoot =
|
|
4109
|
+
const localeRoot = join5(root, rel);
|
|
3937
4110
|
if (!safeIsDir(localeRoot)) continue;
|
|
3938
4111
|
const locales = listDirs(localeRoot).filter(
|
|
3939
|
-
(d) => LOCALE_RE.test(d) && readdirSync3(
|
|
4112
|
+
(d) => LOCALE_RE.test(d) && readdirSync3(join5(localeRoot, d)).some((f) => f.endsWith(".json"))
|
|
3940
4113
|
);
|
|
3941
4114
|
if (locales.length === 0) continue;
|
|
3942
4115
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
3943
4116
|
try {
|
|
3944
|
-
return readdirSync3(
|
|
4117
|
+
return readdirSync3(join5(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync2(join5(localeRoot, loc, f)).size, 0);
|
|
3945
4118
|
} catch {
|
|
3946
4119
|
return 0;
|
|
3947
4120
|
}
|
|
@@ -3958,8 +4131,8 @@ function gettextLocales(dir) {
|
|
|
3958
4131
|
if (!locales.includes(flat)) locales.push(flat);
|
|
3959
4132
|
continue;
|
|
3960
4133
|
}
|
|
3961
|
-
if (!LOCALE_RE.test(entry) || !safeIsDir(
|
|
3962
|
-
const sub =
|
|
4134
|
+
if (!LOCALE_RE.test(entry) || !safeIsDir(join5(dir, entry))) continue;
|
|
4135
|
+
const sub = join5(dir, entry);
|
|
3963
4136
|
const hasPo = (d) => {
|
|
3964
4137
|
try {
|
|
3965
4138
|
return readdirSync3(d).some((f) => f.endsWith(".po"));
|
|
@@ -3967,7 +4140,7 @@ function gettextLocales(dir) {
|
|
|
3967
4140
|
return false;
|
|
3968
4141
|
}
|
|
3969
4142
|
};
|
|
3970
|
-
if (hasPo(
|
|
4143
|
+
if (hasPo(join5(sub, "LC_MESSAGES")) || hasPo(sub)) {
|
|
3971
4144
|
if (!locales.includes(entry)) locales.push(entry);
|
|
3972
4145
|
}
|
|
3973
4146
|
}
|
|
@@ -3976,7 +4149,7 @@ function gettextLocales(dir) {
|
|
|
3976
4149
|
var GETTEXT_DIR_CANDIDATES = ["locale", "locales", "po", "translations"];
|
|
3977
4150
|
function detectGettext(root) {
|
|
3978
4151
|
for (const rel of GETTEXT_DIR_CANDIDATES) {
|
|
3979
|
-
const localeRoot =
|
|
4152
|
+
const localeRoot = join5(root, rel);
|
|
3980
4153
|
if (!safeIsDir(localeRoot)) continue;
|
|
3981
4154
|
const locales = gettextLocales(localeRoot);
|
|
3982
4155
|
if (locales.length === 0) continue;
|
|
@@ -3985,10 +4158,10 @@ function detectGettext(root) {
|
|
|
3985
4158
|
return null;
|
|
3986
4159
|
}
|
|
3987
4160
|
function detectAppleStringsdict(root) {
|
|
3988
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
4161
|
+
const candidates = [root, ...listDirs(root).map((d) => join5(root, d))];
|
|
3989
4162
|
let best = null;
|
|
3990
4163
|
for (const dir of candidates) {
|
|
3991
|
-
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) &&
|
|
4164
|
+
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync10(join5(dir, `${l}.lproj`, "Localizable.stringsdict")));
|
|
3992
4165
|
if (locales.length === 0) continue;
|
|
3993
4166
|
if (!best || locales.length > best.locales.length) {
|
|
3994
4167
|
best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
@@ -4019,7 +4192,7 @@ var BY_FORMAT = {
|
|
|
4019
4192
|
"apple-stringsdict": detectAppleStringsdict
|
|
4020
4193
|
};
|
|
4021
4194
|
function detect(root, formatOverride) {
|
|
4022
|
-
if (!
|
|
4195
|
+
if (!existsSync10(root)) return null;
|
|
4023
4196
|
if (formatOverride) {
|
|
4024
4197
|
const fn = BY_FORMAT[formatOverride];
|
|
4025
4198
|
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
@@ -4033,8 +4206,8 @@ function detect(root, formatOverride) {
|
|
|
4033
4206
|
}
|
|
4034
4207
|
|
|
4035
4208
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
4036
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
4037
|
-
import { join as
|
|
4209
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
|
|
4210
|
+
import { join as join6 } from "path";
|
|
4038
4211
|
|
|
4039
4212
|
// src/server/import/flatten.ts
|
|
4040
4213
|
function flattenObject(value, prefix, warnings) {
|
|
@@ -4073,7 +4246,7 @@ var vueI18nJson2 = {
|
|
|
4073
4246
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4074
4247
|
let data;
|
|
4075
4248
|
try {
|
|
4076
|
-
data = JSON.parse(
|
|
4249
|
+
data = JSON.parse(readFileSync11(join6(localeRoot, file), "utf8"));
|
|
4077
4250
|
} catch (e) {
|
|
4078
4251
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
4079
4252
|
continue;
|
|
@@ -4089,7 +4262,7 @@ var vueI18nJson2 = {
|
|
|
4089
4262
|
|
|
4090
4263
|
// src/server/import/parsers/laravel-php.ts
|
|
4091
4264
|
import { readdirSync as readdirSync5, statSync as statSync3 } from "fs";
|
|
4092
|
-
import { join as
|
|
4265
|
+
import { join as join7, relative as relative2 } from "path";
|
|
4093
4266
|
import { execFileSync } from "child_process";
|
|
4094
4267
|
|
|
4095
4268
|
// src/server/import/placeholders.ts
|
|
@@ -4099,13 +4272,13 @@ function laravelToCanonical(value) {
|
|
|
4099
4272
|
|
|
4100
4273
|
// src/server/import/parsers/laravel-php.ts
|
|
4101
4274
|
function listDirs2(dir) {
|
|
4102
|
-
return readdirSync5(dir).filter((e) => statSync3(
|
|
4275
|
+
return readdirSync5(dir).filter((e) => statSync3(join7(dir, e)).isDirectory());
|
|
4103
4276
|
}
|
|
4104
4277
|
function listPhpFiles(dir) {
|
|
4105
4278
|
const out = [];
|
|
4106
4279
|
const walk = (d) => {
|
|
4107
4280
|
for (const e of readdirSync5(d)) {
|
|
4108
|
-
const full =
|
|
4281
|
+
const full = join7(d, e);
|
|
4109
4282
|
if (statSync3(full).isDirectory()) walk(full);
|
|
4110
4283
|
else if (e.endsWith(".php")) out.push(full);
|
|
4111
4284
|
}
|
|
@@ -4142,7 +4315,7 @@ var laravelPhp2 = {
|
|
|
4142
4315
|
for (const locale of listDirs2(localeRoot).sort()) {
|
|
4143
4316
|
if (locale === "vendor") continue;
|
|
4144
4317
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4145
|
-
const localeDir =
|
|
4318
|
+
const localeDir = join7(localeRoot, locale);
|
|
4146
4319
|
locales.push(locale);
|
|
4147
4320
|
for (const file of listPhpFiles(localeDir)) {
|
|
4148
4321
|
const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
|
|
@@ -4165,8 +4338,8 @@ var laravelPhp2 = {
|
|
|
4165
4338
|
};
|
|
4166
4339
|
|
|
4167
4340
|
// src/server/import/parsers/flutter-arb.ts
|
|
4168
|
-
import { readdirSync as readdirSync6, readFileSync as
|
|
4169
|
-
import { join as
|
|
4341
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync12 } from "fs";
|
|
4342
|
+
import { join as join8 } from "path";
|
|
4170
4343
|
var LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4171
4344
|
function localeFromArbName(file) {
|
|
4172
4345
|
const m = file.match(/^(.+)\.arb$/);
|
|
@@ -4202,7 +4375,7 @@ var flutterArb2 = {
|
|
|
4202
4375
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4203
4376
|
let data;
|
|
4204
4377
|
try {
|
|
4205
|
-
data = JSON.parse(
|
|
4378
|
+
data = JSON.parse(readFileSync12(join8(localeRoot, file), "utf8"));
|
|
4206
4379
|
} catch (e) {
|
|
4207
4380
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
4208
4381
|
continue;
|
|
@@ -4227,8 +4400,8 @@ var flutterArb2 = {
|
|
|
4227
4400
|
};
|
|
4228
4401
|
|
|
4229
4402
|
// src/server/import/parsers/apple-strings.ts
|
|
4230
|
-
import { readdirSync as readdirSync7, readFileSync as
|
|
4231
|
-
import { join as
|
|
4403
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync13, statSync as statSync4 } from "fs";
|
|
4404
|
+
import { join as join9 } from "path";
|
|
4232
4405
|
var LOCALE_RE4 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4233
4406
|
var TABLE = "Localizable.strings";
|
|
4234
4407
|
function localeFromLproj(dir) {
|
|
@@ -4332,16 +4505,16 @@ var appleStrings2 = {
|
|
|
4332
4505
|
const locale = localeFromLproj(dir);
|
|
4333
4506
|
if (!locale) continue;
|
|
4334
4507
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4335
|
-
const file =
|
|
4508
|
+
const file = join9(localeRoot, dir, TABLE);
|
|
4336
4509
|
let text;
|
|
4337
4510
|
try {
|
|
4338
4511
|
if (!statSync4(file).isFile()) continue;
|
|
4339
|
-
text =
|
|
4512
|
+
text = readFileSync13(file, "utf8");
|
|
4340
4513
|
} catch {
|
|
4341
4514
|
continue;
|
|
4342
4515
|
}
|
|
4343
4516
|
locales.push(locale);
|
|
4344
|
-
const others = readdirSync7(
|
|
4517
|
+
const others = readdirSync7(join9(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
|
|
4345
4518
|
if (others.length) {
|
|
4346
4519
|
warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
|
|
4347
4520
|
}
|
|
@@ -4354,8 +4527,8 @@ var appleStrings2 = {
|
|
|
4354
4527
|
};
|
|
4355
4528
|
|
|
4356
4529
|
// src/server/import/parsers/angular-xliff.ts
|
|
4357
|
-
import { readdirSync as readdirSync8, readFileSync as
|
|
4358
|
-
import { join as
|
|
4530
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync14 } from "fs";
|
|
4531
|
+
import { join as join10 } from "path";
|
|
4359
4532
|
var LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4360
4533
|
var FILE_RE = /^messages(?:\.(.+))?\.xlf$/;
|
|
4361
4534
|
function decodeEntities(s) {
|
|
@@ -4403,7 +4576,7 @@ var angularXliff2 = {
|
|
|
4403
4576
|
if (fnameLocale !== void 0 && !LOCALE_RE5.test(fnameLocale)) continue;
|
|
4404
4577
|
let xml;
|
|
4405
4578
|
try {
|
|
4406
|
-
xml =
|
|
4579
|
+
xml = readFileSync14(join10(localeRoot, file), "utf8");
|
|
4407
4580
|
} catch (e) {
|
|
4408
4581
|
warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
|
|
4409
4582
|
continue;
|
|
@@ -4444,8 +4617,8 @@ var angularXliff2 = {
|
|
|
4444
4617
|
};
|
|
4445
4618
|
|
|
4446
4619
|
// src/server/import/parsers/gettext-po.ts
|
|
4447
|
-
import { readdirSync as readdirSync9, readFileSync as
|
|
4448
|
-
import { join as
|
|
4620
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync15 } from "fs";
|
|
4621
|
+
import { join as join11 } from "path";
|
|
4449
4622
|
var LOCALE_RE6 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4450
4623
|
var DIRECTIVE_RE = /^(msgctxt|msgid_plural|msgid|msgstr)(?:\[(\d+)\])?[ \t]+"(.*)"\s*$/;
|
|
4451
4624
|
var CONT_RE = /^[ \t]*"(.*)"\s*$/;
|
|
@@ -4519,17 +4692,17 @@ function discoverPoFiles(root) {
|
|
|
4519
4692
|
for (const e of entries) {
|
|
4520
4693
|
if (e.isFile() && e.name.endsWith(".po")) {
|
|
4521
4694
|
const base = e.name.slice(0, -3);
|
|
4522
|
-
found.push({ path:
|
|
4695
|
+
found.push({ path: join11(root, e.name), rel: e.name, locale: LOCALE_RE6.test(base) ? base : null });
|
|
4523
4696
|
} else if (e.isDirectory() && LOCALE_RE6.test(e.name)) {
|
|
4524
|
-
for (const sub of [
|
|
4697
|
+
for (const sub of [join11(e.name, "LC_MESSAGES"), e.name]) {
|
|
4525
4698
|
let names;
|
|
4526
4699
|
try {
|
|
4527
|
-
names = readdirSync9(
|
|
4700
|
+
names = readdirSync9(join11(root, sub)).sort();
|
|
4528
4701
|
} catch {
|
|
4529
4702
|
continue;
|
|
4530
4703
|
}
|
|
4531
4704
|
for (const f of names) {
|
|
4532
|
-
if (f.endsWith(".po")) found.push({ path:
|
|
4705
|
+
if (f.endsWith(".po")) found.push({ path: join11(root, sub, f), rel: join11(sub, f), locale: e.name });
|
|
4533
4706
|
}
|
|
4534
4707
|
}
|
|
4535
4708
|
}
|
|
@@ -4545,7 +4718,7 @@ var gettextPo2 = {
|
|
|
4545
4718
|
for (const file of discoverPoFiles(localeRoot)) {
|
|
4546
4719
|
let entries;
|
|
4547
4720
|
try {
|
|
4548
|
-
entries = parseEntries(
|
|
4721
|
+
entries = parseEntries(readFileSync15(file.path, "utf8"));
|
|
4549
4722
|
} catch (e) {
|
|
4550
4723
|
warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
|
|
4551
4724
|
continue;
|
|
@@ -4590,8 +4763,8 @@ var gettextPo2 = {
|
|
|
4590
4763
|
};
|
|
4591
4764
|
|
|
4592
4765
|
// src/server/import/parsers/i18next-json.ts
|
|
4593
|
-
import { readdirSync as readdirSync10, readFileSync as
|
|
4594
|
-
import { join as
|
|
4766
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync16, statSync as statSync5 } from "fs";
|
|
4767
|
+
import { join as join12 } from "path";
|
|
4595
4768
|
var LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4596
4769
|
var PLURAL_SUFFIX_RE = /^(.+)_(zero|one|two|few|many|other)$/;
|
|
4597
4770
|
var PLURAL_ARG = "count";
|
|
@@ -4610,7 +4783,7 @@ function fromI18next(value) {
|
|
|
4610
4783
|
function ingestFile(path, label, prefix, locale, keys, warnings) {
|
|
4611
4784
|
let data;
|
|
4612
4785
|
try {
|
|
4613
|
-
data = JSON.parse(
|
|
4786
|
+
data = JSON.parse(readFileSync16(path, "utf8"));
|
|
4614
4787
|
} catch (e) {
|
|
4615
4788
|
warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
|
|
4616
4789
|
return false;
|
|
@@ -4652,7 +4825,7 @@ var i18nextJson2 = {
|
|
|
4652
4825
|
const keys = {};
|
|
4653
4826
|
const locales = [];
|
|
4654
4827
|
for (const entry of readdirSync10(localeRoot).sort()) {
|
|
4655
|
-
const full =
|
|
4828
|
+
const full = join12(localeRoot, entry);
|
|
4656
4829
|
if (safeIsDir2(full)) {
|
|
4657
4830
|
if (!LOCALE_RE7.test(entry)) continue;
|
|
4658
4831
|
if (opts?.locales && !opts.locales.includes(entry)) continue;
|
|
@@ -4661,7 +4834,7 @@ var i18nextJson2 = {
|
|
|
4661
4834
|
if (!file.endsWith(".json")) continue;
|
|
4662
4835
|
const ns = file.slice(0, -".json".length);
|
|
4663
4836
|
const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
|
|
4664
|
-
if (ingestFile(
|
|
4837
|
+
if (ingestFile(join12(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
|
|
4665
4838
|
}
|
|
4666
4839
|
if (any && !locales.includes(entry)) locales.push(entry);
|
|
4667
4840
|
} else if (entry.endsWith(".json")) {
|
|
@@ -4678,8 +4851,8 @@ var i18nextJson2 = {
|
|
|
4678
4851
|
};
|
|
4679
4852
|
|
|
4680
4853
|
// src/server/import/parsers/rails-yaml.ts
|
|
4681
|
-
import { readdirSync as readdirSync11, readFileSync as
|
|
4682
|
-
import { join as
|
|
4854
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync17 } from "fs";
|
|
4855
|
+
import { join as join13 } from "path";
|
|
4683
4856
|
var LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
|
|
4684
4857
|
var CATEGORY_SET = new Set(PLURAL_CATEGORIES);
|
|
4685
4858
|
function fromRuby(value) {
|
|
@@ -4891,7 +5064,7 @@ var railsYaml2 = {
|
|
|
4891
5064
|
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
4892
5065
|
let text;
|
|
4893
5066
|
try {
|
|
4894
|
-
text =
|
|
5067
|
+
text = readFileSync17(join13(localeRoot, file), "utf8");
|
|
4895
5068
|
} catch (e) {
|
|
4896
5069
|
warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
|
|
4897
5070
|
continue;
|
|
@@ -4912,8 +5085,8 @@ var railsYaml2 = {
|
|
|
4912
5085
|
};
|
|
4913
5086
|
|
|
4914
5087
|
// src/server/import/parsers/apple-stringsdict.ts
|
|
4915
|
-
import { readdirSync as readdirSync12, readFileSync as
|
|
4916
|
-
import { join as
|
|
5088
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync18, statSync as statSync6 } from "fs";
|
|
5089
|
+
import { join as join14 } from "path";
|
|
4917
5090
|
var LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4918
5091
|
var TABLE2 = "Localizable.stringsdict";
|
|
4919
5092
|
function localeFromLproj2(dir) {
|
|
@@ -5049,16 +5222,16 @@ var appleStringsdict2 = {
|
|
|
5049
5222
|
const locale = localeFromLproj2(dir);
|
|
5050
5223
|
if (!locale) continue;
|
|
5051
5224
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5052
|
-
const file =
|
|
5225
|
+
const file = join14(localeRoot, dir, TABLE2);
|
|
5053
5226
|
let text;
|
|
5054
5227
|
try {
|
|
5055
5228
|
if (!statSync6(file).isFile()) continue;
|
|
5056
|
-
text =
|
|
5229
|
+
text = readFileSync18(file, "utf8");
|
|
5057
5230
|
} catch {
|
|
5058
5231
|
continue;
|
|
5059
5232
|
}
|
|
5060
5233
|
locales.push(locale);
|
|
5061
|
-
const others = readdirSync12(
|
|
5234
|
+
const others = readdirSync12(join14(localeRoot, dir)).filter(
|
|
5062
5235
|
(f) => f.endsWith(".stringsdict") && f !== TABLE2
|
|
5063
5236
|
);
|
|
5064
5237
|
if (others.length) {
|
|
@@ -5231,7 +5404,7 @@ function runImport(opts) {
|
|
|
5231
5404
|
}
|
|
5232
5405
|
|
|
5233
5406
|
// src/server/export-run.ts
|
|
5234
|
-
import { existsSync as
|
|
5407
|
+
import { existsSync as existsSync11, readFileSync as readFileSync19, readdirSync as readdirSync13, rmdirSync, statSync as statSync7, unlinkSync } from "fs";
|
|
5235
5408
|
import { dirname as dirname2, resolve as resolve7, sep } from "path";
|
|
5236
5409
|
function effectiveLocales(config) {
|
|
5237
5410
|
const limit = config.exportLocales;
|
|
@@ -5274,7 +5447,7 @@ function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
|
|
|
5274
5447
|
if (!segment.includes("{locale}") && !segment.includes("{namespace}")) {
|
|
5275
5448
|
const next = resolve7(dir, segment);
|
|
5276
5449
|
if (isLast) {
|
|
5277
|
-
if (stale(locale) &&
|
|
5450
|
+
if (stale(locale) && existsSync11(next) && statSync7(next).isFile()) {
|
|
5278
5451
|
unlinkSync(next);
|
|
5279
5452
|
deleted++;
|
|
5280
5453
|
removeEmptyDirs(dir, root);
|
|
@@ -5330,7 +5503,7 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
5330
5503
|
writtenPaths.add(abs);
|
|
5331
5504
|
let current = null;
|
|
5332
5505
|
try {
|
|
5333
|
-
current =
|
|
5506
|
+
current = readFileSync19(abs, "utf8");
|
|
5334
5507
|
} catch {
|
|
5335
5508
|
}
|
|
5336
5509
|
if (current === f.contents) {
|
|
@@ -5347,17 +5520,17 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
5347
5520
|
}
|
|
5348
5521
|
|
|
5349
5522
|
// src/server/ui-prefs.ts
|
|
5350
|
-
import { readFileSync as
|
|
5523
|
+
import { readFileSync as readFileSync20 } from "fs";
|
|
5351
5524
|
import { homedir } from "os";
|
|
5352
|
-
import { join as
|
|
5525
|
+
import { join as join15 } from "path";
|
|
5353
5526
|
var THEMES = ["system", "light", "dark"];
|
|
5354
5527
|
var isThemeMode = (v) => THEMES.includes(v);
|
|
5355
5528
|
var isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
|
|
5356
|
-
var defaultUiPrefsPath = () =>
|
|
5529
|
+
var defaultUiPrefsPath = () => join15(homedir(), ".glotfile", "ui.json");
|
|
5357
5530
|
var DEFAULTS = { theme: "system" };
|
|
5358
5531
|
function readJson(path) {
|
|
5359
5532
|
try {
|
|
5360
|
-
const parsed = JSON.parse(
|
|
5533
|
+
const parsed = JSON.parse(readFileSync20(path, "utf8"));
|
|
5361
5534
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
5362
5535
|
} catch {
|
|
5363
5536
|
return {};
|
|
@@ -5376,7 +5549,7 @@ function saveUiPrefs(path, prefs) {
|
|
|
5376
5549
|
}
|
|
5377
5550
|
|
|
5378
5551
|
// src/server/local-settings.ts
|
|
5379
|
-
import { readFileSync as
|
|
5552
|
+
import { readFileSync as readFileSync21 } from "fs";
|
|
5380
5553
|
import { resolve as resolve8 } from "path";
|
|
5381
5554
|
var EDITOR_IDS = ["vscode", "zed", "phpstorm"];
|
|
5382
5555
|
var isEditorId = (v) => EDITOR_IDS.includes(v);
|
|
@@ -5391,7 +5564,7 @@ var DEFAULT_EDITOR = "vscode";
|
|
|
5391
5564
|
var settingsPath = (projectRoot) => resolve8(projectRoot, ".glotfile", "settings.json");
|
|
5392
5565
|
function readJson2(path) {
|
|
5393
5566
|
try {
|
|
5394
|
-
const parsed = JSON.parse(
|
|
5567
|
+
const parsed = JSON.parse(readFileSync21(path, "utf8"));
|
|
5395
5568
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
5396
5569
|
} catch {
|
|
5397
5570
|
return {};
|
|
@@ -5462,9 +5635,9 @@ var sanitize = (s) => s.replace(/[^\w.\-]+/g, "_");
|
|
|
5462
5635
|
var screenshotDirName = (statePath) => basename(statePath).replace(/\.[^.]+$/, "") + "-screenshots";
|
|
5463
5636
|
function projectName(root) {
|
|
5464
5637
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
5465
|
-
if (
|
|
5638
|
+
if (existsSync12(nameFile)) {
|
|
5466
5639
|
try {
|
|
5467
|
-
const name =
|
|
5640
|
+
const name = readFileSync22(nameFile, "utf8").trim();
|
|
5468
5641
|
if (name) return name;
|
|
5469
5642
|
} catch {
|
|
5470
5643
|
}
|
|
@@ -5597,7 +5770,7 @@ function createApi(deps) {
|
|
|
5597
5770
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
5598
5771
|
const abs = resolve9(dir, name);
|
|
5599
5772
|
let filePath = null;
|
|
5600
|
-
if ((name === "glotfile" || name.endsWith(".glotfile")) &&
|
|
5773
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync12(resolve9(abs, "config.json"))) {
|
|
5601
5774
|
filePath = resolve9(dir, `${name}.json`);
|
|
5602
5775
|
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
5603
5776
|
filePath = abs;
|
|
@@ -5631,7 +5804,7 @@ function createApi(deps) {
|
|
|
5631
5804
|
const resolved = resolve9(projectRoot, path);
|
|
5632
5805
|
const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
|
|
5633
5806
|
if (!inside) return c.json({ error: "file is outside the project" }, 400);
|
|
5634
|
-
if (!
|
|
5807
|
+
if (!existsSync12(resolved)) return c.json({ error: "file not found" }, 400);
|
|
5635
5808
|
loadState(resolved);
|
|
5636
5809
|
deps.statePath = resolved;
|
|
5637
5810
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
@@ -5692,9 +5865,9 @@ function createApi(deps) {
|
|
|
5692
5865
|
const abs = resolve9(root, screenshot);
|
|
5693
5866
|
const rel = relative4(root, abs);
|
|
5694
5867
|
const seg0 = rel.split(sep2)[0] ?? "";
|
|
5695
|
-
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") &&
|
|
5868
|
+
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync12(abs)) {
|
|
5696
5869
|
try {
|
|
5697
|
-
|
|
5870
|
+
rmSync5(abs);
|
|
5698
5871
|
} catch {
|
|
5699
5872
|
}
|
|
5700
5873
|
}
|
|
@@ -6232,6 +6405,104 @@ function createApi(deps) {
|
|
|
6232
6405
|
const ai = loadLocalSettings(projectRoot).ai;
|
|
6233
6406
|
return c.json(estimateTranslation(load(), ai, { onlyMissing: body.onlyMissing ?? true, keys, locales }));
|
|
6234
6407
|
});
|
|
6408
|
+
app.get("/batch/status", async (c) => {
|
|
6409
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
6410
|
+
let supported = false;
|
|
6411
|
+
let provider;
|
|
6412
|
+
try {
|
|
6413
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
6414
|
+
supported = supportsBatchTranslate(provider);
|
|
6415
|
+
} catch {
|
|
6416
|
+
}
|
|
6417
|
+
const pending = loadPendingBatch(projectRoot);
|
|
6418
|
+
if (!pending) return c.json({ supported, pending: null });
|
|
6419
|
+
const base = { batchId: pending.batchId, createdAt: pending.createdAt, model: pending.model, total: pending.total };
|
|
6420
|
+
if (!provider || !supportsBatchTranslate(provider)) {
|
|
6421
|
+
return c.json({ supported, pending: { ...base, status: "unknown", counts: null } });
|
|
6422
|
+
}
|
|
6423
|
+
try {
|
|
6424
|
+
const status = await provider.translationBatchStatus(pending.batchId);
|
|
6425
|
+
return c.json({ supported, pending: { ...base, status: status.status, counts: status.counts } });
|
|
6426
|
+
} catch (e) {
|
|
6427
|
+
return c.json({ supported, pending: { ...base, status: "unknown", counts: null, error: e.message } });
|
|
6428
|
+
}
|
|
6429
|
+
});
|
|
6430
|
+
app.post("/batch/translate", (c) => withTranslateLock(async () => {
|
|
6431
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6432
|
+
const s = load();
|
|
6433
|
+
const reqs = selectRequests(s, {
|
|
6434
|
+
onlyMissing: body.onlyMissing ?? true,
|
|
6435
|
+
keys: Array.isArray(body.keys) && body.keys.length ? body.keys.filter(Boolean) : void 0,
|
|
6436
|
+
locales: Array.isArray(body.locales) && body.locales.length ? body.locales.filter(Boolean) : void 0
|
|
6437
|
+
});
|
|
6438
|
+
if (!reqs.length) return c.json({ error: "Nothing to translate." }, 400);
|
|
6439
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
6440
|
+
let provider;
|
|
6441
|
+
try {
|
|
6442
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
6443
|
+
} catch (e) {
|
|
6444
|
+
return c.json({ error: e.message }, 400);
|
|
6445
|
+
}
|
|
6446
|
+
if (!supportsBatchTranslate(provider)) {
|
|
6447
|
+
return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
|
|
6448
|
+
}
|
|
6449
|
+
attachScreenshotsForProvider(reqs, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
6450
|
+
let pending;
|
|
6451
|
+
try {
|
|
6452
|
+
pending = await submitBatchTranslation(s, provider, reqs, aiCfg.batchSize, aiCfg.model, projectRoot);
|
|
6453
|
+
} catch (e) {
|
|
6454
|
+
return c.json({ error: e.message }, 409);
|
|
6455
|
+
}
|
|
6456
|
+
appendLog(projectRoot, {
|
|
6457
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6458
|
+
kind: "translate",
|
|
6459
|
+
summary: `Submitted batch ${pending.batchId} (${pending.total} items)`,
|
|
6460
|
+
model: aiCfg.model,
|
|
6461
|
+
system: buildSystemPrompt(reqs.some((r) => r.plural !== void 0)),
|
|
6462
|
+
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 }))
|
|
6463
|
+
});
|
|
6464
|
+
console.log(`[batch] submitted ${pending.batchId} \u2014 ${pending.total} string(s)`);
|
|
6465
|
+
return c.json({ batchId: pending.batchId, total: pending.total });
|
|
6466
|
+
}));
|
|
6467
|
+
app.post("/batch/apply", (c) => withTranslateLock(async () => {
|
|
6468
|
+
const pending = loadPendingBatch(projectRoot);
|
|
6469
|
+
if (!pending) return c.json({ error: "No pending batch." }, 404);
|
|
6470
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
6471
|
+
let provider;
|
|
6472
|
+
try {
|
|
6473
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
6474
|
+
} catch (e) {
|
|
6475
|
+
return c.json({ error: e.message }, 400);
|
|
6476
|
+
}
|
|
6477
|
+
if (!supportsBatchTranslate(provider)) {
|
|
6478
|
+
return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
|
|
6479
|
+
}
|
|
6480
|
+
const outcome = await applyBatchResults(load, persist, provider, pending, projectRoot, {
|
|
6481
|
+
batchSize: aiCfg.batchSize,
|
|
6482
|
+
concurrency: aiCfg.concurrency
|
|
6483
|
+
});
|
|
6484
|
+
appendLog(projectRoot, {
|
|
6485
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6486
|
+
kind: "translate",
|
|
6487
|
+
summary: `Applied batch ${pending.batchId}: wrote ${outcome.written}, ${outcome.retried} retried, ${outcome.staleSkipped} stale`,
|
|
6488
|
+
model: aiCfg.model,
|
|
6489
|
+
results: []
|
|
6490
|
+
});
|
|
6491
|
+
console.log(`[batch] applied ${pending.batchId} \u2014 wrote ${outcome.written}, ${outcome.errors.length} error(s)`);
|
|
6492
|
+
return c.json(outcome);
|
|
6493
|
+
}));
|
|
6494
|
+
app.post("/batch/cancel", async (c) => {
|
|
6495
|
+
const pending = loadPendingBatch(projectRoot);
|
|
6496
|
+
if (!pending) return c.json({ error: "No pending batch." }, 404);
|
|
6497
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
6498
|
+
try {
|
|
6499
|
+
const provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
6500
|
+
if (supportsBatchTranslate(provider)) await provider.cancelTranslationBatch(pending.batchId);
|
|
6501
|
+
} catch {
|
|
6502
|
+
}
|
|
6503
|
+
clearPendingBatch(projectRoot);
|
|
6504
|
+
return c.json({ canceled: pending.batchId });
|
|
6505
|
+
});
|
|
6235
6506
|
app.get("/log", (c) => c.json(readLog(projectRoot, 100)));
|
|
6236
6507
|
app.post("/scan", async (c) => {
|
|
6237
6508
|
const s = load();
|
|
@@ -6397,7 +6668,7 @@ function createApi(deps) {
|
|
|
6397
6668
|
|
|
6398
6669
|
// src/server/server.ts
|
|
6399
6670
|
var here = dirname4(fileURLToPath(import.meta.url));
|
|
6400
|
-
var DEFAULT_UI_DIR =
|
|
6671
|
+
var DEFAULT_UI_DIR = join16(here, "..", "ui");
|
|
6401
6672
|
var MIME = {
|
|
6402
6673
|
".html": "text/html; charset=utf-8",
|
|
6403
6674
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -6451,7 +6722,7 @@ function buildApp(opts) {
|
|
|
6451
6722
|
const file = await readFileResponse(target);
|
|
6452
6723
|
if (file) return file;
|
|
6453
6724
|
}
|
|
6454
|
-
const index = await readFileResponse(
|
|
6725
|
+
const index = await readFileResponse(join16(root, "index.html"));
|
|
6455
6726
|
if (index) return index;
|
|
6456
6727
|
return c.notFound();
|
|
6457
6728
|
});
|