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