glotfile 0.8.1 → 0.8.3
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
CHANGED
|
@@ -690,7 +690,7 @@ function setSourceValue(state, key, value) {
|
|
|
690
690
|
function setTargetValue(state, key, locale, value, clock = systemClock) {
|
|
691
691
|
const entry = requireKey(state, key);
|
|
692
692
|
if (entry.plural) throw new GlotfileError(`Key is a plural; use the plural setters: ${key}`);
|
|
693
|
-
entry.values[locale] = { value: value.trim(), state: "reviewed", updatedAt: clock() };
|
|
693
|
+
entry.values[canonLocale(locale)] = { value: value.trim(), state: "reviewed", updatedAt: clock() };
|
|
694
694
|
}
|
|
695
695
|
function formSignature(forms) {
|
|
696
696
|
return Object.entries(forms).sort(([a], [b]) => a.localeCompare(b)).map(([cat, val]) => `${cat}:${normalizeSource(val ?? "")}`).join("|");
|
|
@@ -713,8 +713,9 @@ function setSourcePluralForms(state, key, forms) {
|
|
|
713
713
|
}
|
|
714
714
|
function setPluralForms(state, key, locale, forms, clock = systemClock) {
|
|
715
715
|
const entry = requirePlural(state, key);
|
|
716
|
-
|
|
717
|
-
|
|
716
|
+
const loc = canonLocale(locale);
|
|
717
|
+
if (loc === state.config.sourceLocale) throw new GlotfileError("Use setSourcePluralForms for the source locale");
|
|
718
|
+
entry.values[loc] = { forms: normalizeForms(forms), state: "reviewed", updatedAt: clock() };
|
|
718
719
|
}
|
|
719
720
|
function convertToPlural(state, key, arg) {
|
|
720
721
|
const entry = requireKey(state, key);
|
|
@@ -742,12 +743,15 @@ function convertToScalar(state, key) {
|
|
|
742
743
|
}
|
|
743
744
|
function clearValue(state, key, locale) {
|
|
744
745
|
const entry = requireKey(state, key);
|
|
745
|
-
|
|
746
|
-
|
|
746
|
+
const loc = canonLocale(locale);
|
|
747
|
+
if (loc === state.config.sourceLocale) throw new GlotfileError("Cannot clear the source value");
|
|
748
|
+
delete entry.values[loc];
|
|
747
749
|
}
|
|
748
750
|
function setKeyState(state, key, locale, next) {
|
|
751
|
+
if (!STATES.includes(next)) throw new GlotfileError(`Unknown translation state: ${next}`);
|
|
749
752
|
const entry = requireKey(state, key);
|
|
750
|
-
const
|
|
753
|
+
const loc = canonLocale(locale);
|
|
754
|
+
const lv = entry.values[loc];
|
|
751
755
|
if (!lv) throw new GlotfileError(`No value for ${key} @ ${locale}`);
|
|
752
756
|
lv.state = next;
|
|
753
757
|
}
|
|
@@ -820,14 +824,16 @@ function removeCustomWord(state, word) {
|
|
|
820
824
|
function applyMachineTranslation(state, key, locale, value, clock = systemClock, force = false) {
|
|
821
825
|
const entry = requireKey(state, key);
|
|
822
826
|
if (entry.plural) throw new GlotfileError(`Key is a plural; use applyMachineTranslationForms: ${key}`);
|
|
823
|
-
|
|
824
|
-
entry.values[
|
|
827
|
+
const loc = canonLocale(locale);
|
|
828
|
+
if (!force && entry.values[loc]?.state === "reviewed") return false;
|
|
829
|
+
entry.values[loc] = { value: value.trim(), state: "machine", source: "ai", updatedAt: clock() };
|
|
825
830
|
return true;
|
|
826
831
|
}
|
|
827
832
|
function applyMachineTranslationForms(state, key, locale, forms, clock = systemClock, force = false) {
|
|
828
833
|
const entry = requirePlural(state, key);
|
|
829
|
-
|
|
830
|
-
entry.values[
|
|
834
|
+
const loc = canonLocale(locale);
|
|
835
|
+
if (!force && entry.values[loc]?.state === "reviewed") return false;
|
|
836
|
+
entry.values[loc] = { forms: normalizeForms(forms), state: "machine", source: "ai", updatedAt: clock() };
|
|
831
837
|
return true;
|
|
832
838
|
}
|
|
833
839
|
var systemClock;
|
|
@@ -1245,28 +1251,33 @@ var init_i18next_json = __esm({
|
|
|
1245
1251
|
export(state, output) {
|
|
1246
1252
|
const files = [];
|
|
1247
1253
|
const warnings = [];
|
|
1248
|
-
const
|
|
1254
|
+
const { indent, finalNewline } = resolveFormat(state, output);
|
|
1255
|
+
const fmt = { indent, sortKeys: true, finalNewline };
|
|
1256
|
+
const emptyAs = resolveEmptyAs(output, "omit");
|
|
1249
1257
|
const collided = /* @__PURE__ */ new Set();
|
|
1250
1258
|
warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE3));
|
|
1251
1259
|
for (const locale of state.config.locales) {
|
|
1252
1260
|
const obj = {};
|
|
1253
1261
|
for (const [key, entry] of Object.entries(state.keys)) {
|
|
1254
|
-
const lv = entry.values[locale];
|
|
1255
|
-
if (!lv) continue;
|
|
1256
1262
|
const segments = key.split(".");
|
|
1257
1263
|
const leaf = segments[segments.length - 1];
|
|
1258
1264
|
const parent = segments.slice(0, -1);
|
|
1259
1265
|
if (entry.plural) {
|
|
1260
|
-
|
|
1266
|
+
const forms = resolveForms(entry, locale, state.config.sourceLocale, emptyAs);
|
|
1267
|
+
if (!forms) continue;
|
|
1261
1268
|
for (const cat of PLURAL_CATEGORIES) {
|
|
1262
|
-
const body =
|
|
1269
|
+
const body = forms[cat];
|
|
1263
1270
|
if (body === void 0) continue;
|
|
1264
1271
|
if (setNested(obj, [...parent, `${leaf}_${cat}`], toI18next(body))) collided.add(key);
|
|
1265
1272
|
}
|
|
1266
1273
|
continue;
|
|
1267
1274
|
}
|
|
1268
|
-
|
|
1269
|
-
if (
|
|
1275
|
+
const raw = resolveScalar(entry, locale, state.config.sourceLocale, emptyAs);
|
|
1276
|
+
if (raw === null) continue;
|
|
1277
|
+
if (raw && isIcuPluralOrSelect(raw)) {
|
|
1278
|
+
warnings.push({ code: "lossy-plural", key, locale, message: "i18next-json does not yet convert ICU plural/select; written unconverted" });
|
|
1279
|
+
}
|
|
1280
|
+
if (setNested(obj, segments, toI18next(raw))) collided.add(key);
|
|
1270
1281
|
}
|
|
1271
1282
|
files.push({ path: resolvePath(output.path, resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE3)), contents: serializeJson(obj, fmt) });
|
|
1272
1283
|
}
|
|
@@ -1579,7 +1590,8 @@ function attrEscape(s) {
|
|
|
1579
1590
|
return xmlEscape2(s).replace(/"/g, """);
|
|
1580
1591
|
}
|
|
1581
1592
|
function angularXMeta(placeholders, name) {
|
|
1582
|
-
|
|
1593
|
+
const meta = placeholders?.[name];
|
|
1594
|
+
return /^[A-Z][A-Z0-9_]*$/.test(name) || meta?.origin === "x" ? meta : void 0;
|
|
1583
1595
|
}
|
|
1584
1596
|
function renderInterpolations(text, ids, placeholders) {
|
|
1585
1597
|
let out = "";
|
|
@@ -2464,6 +2476,7 @@ var init_anthropic = __esm({
|
|
|
2464
2476
|
}, { signal });
|
|
2465
2477
|
this.recordUsage(res.usage);
|
|
2466
2478
|
const text = res.content.find((b) => b.type === "text")?.text ?? "";
|
|
2479
|
+
if (res.stop_reason === "max_tokens") throw new MalformedReplyError(text);
|
|
2467
2480
|
return parseReplyItems(text);
|
|
2468
2481
|
}
|
|
2469
2482
|
};
|
|
@@ -2557,6 +2570,7 @@ var init_openai = __esm({
|
|
|
2557
2570
|
]
|
|
2558
2571
|
}, { signal });
|
|
2559
2572
|
const text = res.choices?.[0]?.message?.content ?? "";
|
|
2573
|
+
if (res.choices?.[0]?.finish_reason === "length") throw new MalformedReplyError(text);
|
|
2560
2574
|
return parseReplyItems(text);
|
|
2561
2575
|
}
|
|
2562
2576
|
};
|
|
@@ -2674,8 +2688,11 @@ var init_bedrock = __esm({
|
|
|
2674
2688
|
const res = await this.client.send(this.makeCommand(this.buildInput(batch)), { abortSignal: signal });
|
|
2675
2689
|
const blocks = res.output?.message?.content ?? [];
|
|
2676
2690
|
const tool = blocks.find((b) => b.toolUse)?.toolUse;
|
|
2677
|
-
if (tool?.input?.items) return tool.input.items;
|
|
2678
2691
|
const text = blocks.find((b) => b.text)?.text ?? "";
|
|
2692
|
+
if (res.stopReason === "max_tokens") {
|
|
2693
|
+
throw new MalformedReplyError(text || JSON.stringify(tool?.input ?? {}));
|
|
2694
|
+
}
|
|
2695
|
+
if (tool?.input?.items) return tool.input.items;
|
|
2679
2696
|
return parseReplyItems(text);
|
|
2680
2697
|
}
|
|
2681
2698
|
};
|
|
@@ -3131,8 +3148,30 @@ function attachScreenshotsForProvider(reqs, state, projectRoot, supportsVision)
|
|
|
3131
3148
|
const keys = new Set(reqs.filter((r) => state.keys[r.key]?.screenshot).map((r) => r.key));
|
|
3132
3149
|
return { skipped: keys.size };
|
|
3133
3150
|
}
|
|
3134
|
-
|
|
3151
|
+
function isTransientError(err) {
|
|
3152
|
+
const e = err;
|
|
3153
|
+
if (e && typeof e.status === "number") return e.status === 429 || e.status >= 500;
|
|
3154
|
+
const code = e?.code;
|
|
3155
|
+
return code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ECONNREFUSED" || code === "EPIPE" || code === "EAI_AGAIN";
|
|
3156
|
+
}
|
|
3157
|
+
function sleep(ms, signal) {
|
|
3158
|
+
if (ms <= 0 || signal?.aborted) return Promise.resolve();
|
|
3159
|
+
return new Promise((resolve12) => {
|
|
3160
|
+
const t = setTimeout(() => {
|
|
3161
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3162
|
+
resolve12();
|
|
3163
|
+
}, ms);
|
|
3164
|
+
const onAbort = () => {
|
|
3165
|
+
clearTimeout(t);
|
|
3166
|
+
resolve12();
|
|
3167
|
+
};
|
|
3168
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3169
|
+
});
|
|
3170
|
+
}
|
|
3171
|
+
async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAULT_LOCALE_CONCURRENCY, signal, batchSize = Infinity, retry = {}) {
|
|
3135
3172
|
if (!reqs.length) return [];
|
|
3173
|
+
const maxRetries = retry.retries ?? 3;
|
|
3174
|
+
const delayMs = retry.delayMs ?? ((attempt) => 250 * 2 ** attempt);
|
|
3136
3175
|
const byLocale = /* @__PURE__ */ new Map();
|
|
3137
3176
|
for (const req of reqs) {
|
|
3138
3177
|
let group = byLocale.get(req.targetLocale);
|
|
@@ -3167,10 +3206,20 @@ async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAU
|
|
|
3167
3206
|
started.add(locale);
|
|
3168
3207
|
hooks.onLocaleStart?.(locale);
|
|
3169
3208
|
}
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3209
|
+
let batchResults;
|
|
3210
|
+
for (let attempt = 0; ; attempt++) {
|
|
3211
|
+
try {
|
|
3212
|
+
batchResults = await provider.translate(batch, (_localeDone, _localeTotal, results) => {
|
|
3213
|
+
done += results.length;
|
|
3214
|
+
hooks.onBatchComplete?.(done, total, results, locale);
|
|
3215
|
+
}, signal, (raw, size) => hooks.onMalformedReply?.(raw, size, locale));
|
|
3216
|
+
break;
|
|
3217
|
+
} catch (err) {
|
|
3218
|
+
if (attempt >= maxRetries || signal?.aborted || !isTransientError(err)) throw err;
|
|
3219
|
+
hooks.onRetry?.(locale, attempt + 1, err);
|
|
3220
|
+
await sleep(delayMs(attempt), signal);
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3174
3223
|
allResults.push(...batchResults);
|
|
3175
3224
|
const left = remaining.get(locale) - 1;
|
|
3176
3225
|
remaining.set(locale, left);
|
|
@@ -3261,26 +3310,63 @@ var init_pending_batch = __esm({
|
|
|
3261
3310
|
});
|
|
3262
3311
|
|
|
3263
3312
|
// src/server/log.ts
|
|
3264
|
-
import { appendFileSync,
|
|
3313
|
+
import { appendFileSync, existsSync as existsSync7, openSync, fstatSync, readSync, closeSync, statSync as statSync2, readFileSync as readFileSync7 } from "fs";
|
|
3265
3314
|
import { resolve as resolve5 } from "path";
|
|
3266
3315
|
function logPath(projectRoot) {
|
|
3267
3316
|
return resolve5(projectRoot, ".glotfile", "log.jsonl");
|
|
3268
3317
|
}
|
|
3269
3318
|
function appendLog(projectRoot, entry) {
|
|
3270
3319
|
ensureGlotfileDir(projectRoot);
|
|
3271
|
-
appendFileSync(logPath(projectRoot), JSON.stringify(entry) + "\n", "utf8");
|
|
3272
|
-
}
|
|
3273
|
-
function readLog(projectRoot, limit = 100) {
|
|
3274
3320
|
const path = logPath(projectRoot);
|
|
3275
|
-
|
|
3321
|
+
appendFileSync(path, JSON.stringify(entry) + "\n", "utf8");
|
|
3322
|
+
trimLog(path);
|
|
3323
|
+
}
|
|
3324
|
+
function trimLog(path, maxBytes = MAX_LOG_BYTES, targetBytes = TRIM_LOG_TO_BYTES) {
|
|
3325
|
+
if (!existsSync7(path) || statSync2(path).size <= maxBytes) return;
|
|
3276
3326
|
const lines = readFileSync7(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
3277
|
-
const
|
|
3278
|
-
|
|
3327
|
+
const kept = [];
|
|
3328
|
+
let bytes = 0;
|
|
3329
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3330
|
+
const lineBytes = Buffer.byteLength(lines[i], "utf8") + 1;
|
|
3331
|
+
if (kept.length > 0 && bytes + lineBytes > targetBytes) break;
|
|
3332
|
+
kept.unshift(lines[i]);
|
|
3333
|
+
bytes += lineBytes;
|
|
3334
|
+
}
|
|
3335
|
+
writeFileAtomic(path, kept.length ? kept.join("\n") + "\n" : "");
|
|
3336
|
+
}
|
|
3337
|
+
function readLastLines(path, n, chunkSize = 64 * 1024) {
|
|
3338
|
+
if (n <= 0 || !existsSync7(path)) return [];
|
|
3339
|
+
const fd = openSync(path, "r");
|
|
3340
|
+
try {
|
|
3341
|
+
let pos = fstatSync(fd).size;
|
|
3342
|
+
if (pos === 0) return [];
|
|
3343
|
+
const chunks = [];
|
|
3344
|
+
while (pos > 0) {
|
|
3345
|
+
const size = Math.min(chunkSize, pos);
|
|
3346
|
+
pos -= size;
|
|
3347
|
+
const buf = Buffer.alloc(size);
|
|
3348
|
+
readSync(fd, buf, 0, size, pos);
|
|
3349
|
+
chunks.unshift(buf);
|
|
3350
|
+
const segments = Buffer.concat(chunks).toString("utf8").split("\n");
|
|
3351
|
+
const complete = (pos > 0 ? segments.slice(1) : segments).filter((l) => l.trim() !== "");
|
|
3352
|
+
if (complete.length >= n || pos === 0) return complete.slice(-n);
|
|
3353
|
+
}
|
|
3354
|
+
return [];
|
|
3355
|
+
} finally {
|
|
3356
|
+
closeSync(fd);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
function readLog(projectRoot, limit = 100) {
|
|
3360
|
+
return readLastLines(logPath(projectRoot), limit).map((l) => JSON.parse(l)).reverse();
|
|
3279
3361
|
}
|
|
3362
|
+
var MAX_LOG_BYTES, TRIM_LOG_TO_BYTES;
|
|
3280
3363
|
var init_log = __esm({
|
|
3281
3364
|
"src/server/log.ts"() {
|
|
3282
3365
|
"use strict";
|
|
3283
3366
|
init_glotfile_dir();
|
|
3367
|
+
init_atomic_write();
|
|
3368
|
+
MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
3369
|
+
TRIM_LOG_TO_BYTES = 4 * 1024 * 1024;
|
|
3284
3370
|
}
|
|
3285
3371
|
});
|
|
3286
3372
|
|
|
@@ -3872,7 +3958,7 @@ var init_scan = __esm({
|
|
|
3872
3958
|
});
|
|
3873
3959
|
|
|
3874
3960
|
// src/server/scanner.ts
|
|
3875
|
-
import { readdirSync as readdirSync3, statSync as
|
|
3961
|
+
import { readdirSync as readdirSync3, statSync as statSync3, readFileSync as readFileSync11 } from "fs";
|
|
3876
3962
|
import { join as join5, extname as extname2, relative } from "path";
|
|
3877
3963
|
function scannerForExt(ext) {
|
|
3878
3964
|
return EXT_SCANNER[ext] ?? null;
|
|
@@ -4029,7 +4115,7 @@ function* walkFiles(dir, root, exclude) {
|
|
|
4029
4115
|
const rel = relative(root, abs);
|
|
4030
4116
|
let st;
|
|
4031
4117
|
try {
|
|
4032
|
-
st =
|
|
4118
|
+
st = statSync3(abs);
|
|
4033
4119
|
} catch {
|
|
4034
4120
|
continue;
|
|
4035
4121
|
}
|
|
@@ -4058,7 +4144,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
4058
4144
|
const abs = join5(projectRoot, relPath);
|
|
4059
4145
|
let st;
|
|
4060
4146
|
try {
|
|
4061
|
-
st =
|
|
4147
|
+
st = statSync3(abs);
|
|
4062
4148
|
} catch {
|
|
4063
4149
|
continue;
|
|
4064
4150
|
}
|
|
@@ -4109,7 +4195,21 @@ var init_scanner = __esm({
|
|
|
4109
4195
|
// t('key') — word boundary before t, not preceded by dot (excludes i18n.t which is above)
|
|
4110
4196
|
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*'([^']+)'/g,
|
|
4111
4197
|
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*"([^"]+)"/g,
|
|
4112
|
-
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$\n]+)`/g
|
|
4198
|
+
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$\n]+)`/g,
|
|
4199
|
+
// vue-i18n pluralization: $tc('key') and the destructured bare tc('key').
|
|
4200
|
+
/\$tc\s*\(\s*'([^']+)'/g,
|
|
4201
|
+
/\$tc\s*\(\s*"([^"]+)"/g,
|
|
4202
|
+
/(?<!\.)(?<![a-zA-Z0-9_$])\btc\s*\(\s*'([^']+)'/g,
|
|
4203
|
+
/(?<!\.)(?<![a-zA-Z0-9_$])\btc\s*\(\s*"([^"]+)"/g,
|
|
4204
|
+
// React-i18next <Trans i18nKey="key" /> (attribute order tolerated).
|
|
4205
|
+
/<Trans\b[^>]*\bi18nKey\s*=\s*'([^']+)'/g,
|
|
4206
|
+
/<Trans\b[^>]*\bi18nKey\s*=\s*"([^"]+)"/g,
|
|
4207
|
+
// A renamed translate() wrapper (covers the common `const { t: translate }`
|
|
4208
|
+
// alias by name; arbitrary aliases aren't resolved). Method `.translate()`
|
|
4209
|
+
// is excluded. Over-matching here only keeps keys "used" — the safe direction
|
|
4210
|
+
// for prune, which deletes only keys with no match at all.
|
|
4211
|
+
/(?<!\.)(?<![a-zA-Z0-9_$])\btranslate\s*\(\s*'([^']+)'/g,
|
|
4212
|
+
/(?<!\.)(?<![a-zA-Z0-9_$])\btranslate\s*\(\s*"([^"]+)"/g
|
|
4113
4213
|
],
|
|
4114
4214
|
gettext: [
|
|
4115
4215
|
/\b(?:gettext|ngettext)\s*\(\s*'([^']+)'/g,
|
|
@@ -4143,7 +4243,7 @@ var init_scanner = __esm({
|
|
|
4143
4243
|
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$]*)\$\{/g
|
|
4144
4244
|
]
|
|
4145
4245
|
};
|
|
4146
|
-
CACHE_VERSION =
|
|
4246
|
+
CACHE_VERSION = 7;
|
|
4147
4247
|
EXT_SCANNER = {
|
|
4148
4248
|
".php": "laravel",
|
|
4149
4249
|
".vue": "js-i18n",
|
|
@@ -4187,11 +4287,11 @@ var init_scanner = __esm({
|
|
|
4187
4287
|
});
|
|
4188
4288
|
|
|
4189
4289
|
// src/server/import/detect.ts
|
|
4190
|
-
import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as
|
|
4290
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
|
|
4191
4291
|
import { join as join6 } from "path";
|
|
4192
4292
|
function safeIsDir(p) {
|
|
4193
4293
|
try {
|
|
4194
|
-
return
|
|
4294
|
+
return statSync4(p).isDirectory();
|
|
4195
4295
|
} catch {
|
|
4196
4296
|
return false;
|
|
4197
4297
|
}
|
|
@@ -4227,7 +4327,7 @@ function detectVue(root, forced = false) {
|
|
|
4227
4327
|
if (enough) {
|
|
4228
4328
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4229
4329
|
try {
|
|
4230
|
-
return
|
|
4330
|
+
return statSync4(join6(localeRoot, `${loc}.json`)).size;
|
|
4231
4331
|
} catch {
|
|
4232
4332
|
return 0;
|
|
4233
4333
|
}
|
|
@@ -4264,7 +4364,7 @@ function detectApple(root) {
|
|
|
4264
4364
|
locales,
|
|
4265
4365
|
sourceLocale: pickSource(locales, (loc) => {
|
|
4266
4366
|
try {
|
|
4267
|
-
return
|
|
4367
|
+
return statSync4(join6(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
4268
4368
|
} catch {
|
|
4269
4369
|
return 0;
|
|
4270
4370
|
}
|
|
@@ -4324,7 +4424,7 @@ function detectI18next(root) {
|
|
|
4324
4424
|
if (locales.length === 0) continue;
|
|
4325
4425
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4326
4426
|
try {
|
|
4327
|
-
return readdirSync4(join6(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum +
|
|
4427
|
+
return readdirSync4(join6(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync4(join6(localeRoot, loc, f)).size, 0);
|
|
4328
4428
|
} catch {
|
|
4329
4429
|
return 0;
|
|
4330
4430
|
}
|
|
@@ -4429,10 +4529,9 @@ var init_detect = __esm({
|
|
|
4429
4529
|
function flattenObject(value, prefix, warnings) {
|
|
4430
4530
|
const out = {};
|
|
4431
4531
|
const walk = (node, path) => {
|
|
4432
|
-
if (typeof node === "string") {
|
|
4433
|
-
out
|
|
4434
|
-
|
|
4435
|
-
out[path] = String(node);
|
|
4532
|
+
if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") {
|
|
4533
|
+
if (path in out) warnings.push(`duplicate flattened key "${path}" \u2014 keeping the first value`);
|
|
4534
|
+
else out[path] = typeof node === "string" ? node : String(node);
|
|
4436
4535
|
} else if (Array.isArray(node)) {
|
|
4437
4536
|
node.forEach((el, i) => walk(el, path ? `${path}.${i}` : String(i)));
|
|
4438
4537
|
} else if (node && typeof node === "object") {
|
|
@@ -4501,18 +4600,18 @@ var init_placeholders2 = __esm({
|
|
|
4501
4600
|
});
|
|
4502
4601
|
|
|
4503
4602
|
// src/server/import/parsers/laravel-php.ts
|
|
4504
|
-
import { readdirSync as readdirSync6, statSync as
|
|
4603
|
+
import { readdirSync as readdirSync6, statSync as statSync5 } from "fs";
|
|
4505
4604
|
import { join as join8, relative as relative2 } from "path";
|
|
4506
4605
|
import { execFileSync } from "child_process";
|
|
4507
4606
|
function listDirs2(dir) {
|
|
4508
|
-
return readdirSync6(dir).filter((e) =>
|
|
4607
|
+
return readdirSync6(dir).filter((e) => statSync5(join8(dir, e)).isDirectory());
|
|
4509
4608
|
}
|
|
4510
4609
|
function listPhpFiles(dir) {
|
|
4511
4610
|
const out = [];
|
|
4512
4611
|
const walk = (d) => {
|
|
4513
4612
|
for (const e of readdirSync6(d)) {
|
|
4514
4613
|
const full = join8(d, e);
|
|
4515
|
-
if (
|
|
4614
|
+
if (statSync5(full).isDirectory()) walk(full);
|
|
4516
4615
|
else if (e.endsWith(".php")) out.push(full);
|
|
4517
4616
|
}
|
|
4518
4617
|
};
|
|
@@ -4647,7 +4746,7 @@ var init_flutter_arb2 = __esm({
|
|
|
4647
4746
|
});
|
|
4648
4747
|
|
|
4649
4748
|
// src/server/import/parsers/apple-strings.ts
|
|
4650
|
-
import { readdirSync as readdirSync8, readFileSync as readFileSync15, statSync as
|
|
4749
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync15, statSync as statSync6 } from "fs";
|
|
4651
4750
|
import { join as join10 } from "path";
|
|
4652
4751
|
function localeFromLproj(dir) {
|
|
4653
4752
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
@@ -4714,25 +4813,34 @@ function parseStrings(text, file, warnings) {
|
|
|
4714
4813
|
while (i < n && !/[\s=;]/.test(text[i])) raw += text[i++];
|
|
4715
4814
|
return raw.length ? raw : null;
|
|
4716
4815
|
};
|
|
4816
|
+
const recover = () => {
|
|
4817
|
+
while (i < n && text[i] !== ";") i++;
|
|
4818
|
+
if (i >= n) return false;
|
|
4819
|
+
i++;
|
|
4820
|
+
return true;
|
|
4821
|
+
};
|
|
4717
4822
|
while (true) {
|
|
4718
4823
|
skipTrivia();
|
|
4719
4824
|
if (i >= n) break;
|
|
4720
4825
|
const key = readToken();
|
|
4721
4826
|
if (key === null) {
|
|
4722
4827
|
warnings.push(`apple-strings: malformed entry in ${file} near offset ${i}`);
|
|
4723
|
-
break;
|
|
4828
|
+
if (!recover()) break;
|
|
4829
|
+
continue;
|
|
4724
4830
|
}
|
|
4725
4831
|
skipTrivia();
|
|
4726
4832
|
if (text[i] !== "=") {
|
|
4727
4833
|
warnings.push(`apple-strings: expected '=' after key "${key}" in ${file}`);
|
|
4728
|
-
break;
|
|
4834
|
+
if (!recover()) break;
|
|
4835
|
+
continue;
|
|
4729
4836
|
}
|
|
4730
4837
|
i++;
|
|
4731
4838
|
skipTrivia();
|
|
4732
4839
|
const value = readToken();
|
|
4733
4840
|
if (value === null) {
|
|
4734
4841
|
warnings.push(`apple-strings: missing value for key "${key}" in ${file}`);
|
|
4735
|
-
break;
|
|
4842
|
+
if (!recover()) break;
|
|
4843
|
+
continue;
|
|
4736
4844
|
}
|
|
4737
4845
|
skipTrivia();
|
|
4738
4846
|
if (text[i] === ";") i++;
|
|
@@ -4759,7 +4867,7 @@ var init_apple_strings2 = __esm({
|
|
|
4759
4867
|
const file = join10(localeRoot, dir, TABLE);
|
|
4760
4868
|
let text;
|
|
4761
4869
|
try {
|
|
4762
|
-
if (!
|
|
4870
|
+
if (!statSync6(file).isFile()) continue;
|
|
4763
4871
|
text = readFileSync15(file, "utf8");
|
|
4764
4872
|
} catch {
|
|
4765
4873
|
continue;
|
|
@@ -4823,18 +4931,20 @@ function decodeInline(raw, addMeta) {
|
|
|
4823
4931
|
const meta = {};
|
|
4824
4932
|
if (attrs["ctype"]) meta.type = attrs["ctype"];
|
|
4825
4933
|
if (equiv !== void 0) meta.example = equiv;
|
|
4934
|
+
if (!ANGULAR_CONVENTION_ID.test(id)) meta.origin = "x";
|
|
4826
4935
|
addMeta(id, meta);
|
|
4827
4936
|
}
|
|
4828
4937
|
last = m.index + m[0].length;
|
|
4829
4938
|
}
|
|
4830
4939
|
return out + decodeEntities(raw.slice(last));
|
|
4831
4940
|
}
|
|
4832
|
-
var LOCALE_RE5, FILE_RE, angularXliff2;
|
|
4941
|
+
var LOCALE_RE5, FILE_RE, ANGULAR_CONVENTION_ID, angularXliff2;
|
|
4833
4942
|
var init_angular_xliff2 = __esm({
|
|
4834
4943
|
"src/server/import/parsers/angular-xliff.ts"() {
|
|
4835
4944
|
"use strict";
|
|
4836
4945
|
LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4837
4946
|
FILE_RE = /^messages(?:\.(.+))?\.xlf$/;
|
|
4947
|
+
ANGULAR_CONVENTION_ID = /^[A-Z][A-Z0-9_]*$/;
|
|
4838
4948
|
angularXliff2 = {
|
|
4839
4949
|
name: "angular-xliff",
|
|
4840
4950
|
parse(localeRoot, opts) {
|
|
@@ -5050,11 +5160,11 @@ var init_gettext_po2 = __esm({
|
|
|
5050
5160
|
});
|
|
5051
5161
|
|
|
5052
5162
|
// src/server/import/parsers/i18next-json.ts
|
|
5053
|
-
import { readdirSync as readdirSync11, readFileSync as readFileSync18, statSync as
|
|
5163
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync18, statSync as statSync7 } from "fs";
|
|
5054
5164
|
import { join as join13 } from "path";
|
|
5055
5165
|
function safeIsDir2(p) {
|
|
5056
5166
|
try {
|
|
5057
|
-
return
|
|
5167
|
+
return statSync7(p).isDirectory();
|
|
5058
5168
|
} catch {
|
|
5059
5169
|
return false;
|
|
5060
5170
|
}
|
|
@@ -5165,6 +5275,15 @@ function decodeDouble(body) {
|
|
|
5165
5275
|
}
|
|
5166
5276
|
const n = body[++i];
|
|
5167
5277
|
if (n === void 0) break;
|
|
5278
|
+
const hexLen = n === "x" ? 2 : n === "u" ? 4 : n === "U" ? 8 : 0;
|
|
5279
|
+
if (hexLen) {
|
|
5280
|
+
const hex = body.slice(i + 1, i + 1 + hexLen);
|
|
5281
|
+
if (hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex)) {
|
|
5282
|
+
out += String.fromCodePoint(parseInt(hex, 16));
|
|
5283
|
+
i += hexLen;
|
|
5284
|
+
continue;
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5168
5287
|
out += n === "n" ? "\n" : n === "r" ? "\r" : n === "t" ? " " : n;
|
|
5169
5288
|
}
|
|
5170
5289
|
return out;
|
|
@@ -5388,7 +5507,7 @@ var init_rails_yaml2 = __esm({
|
|
|
5388
5507
|
});
|
|
5389
5508
|
|
|
5390
5509
|
// src/server/import/parsers/apple-stringsdict.ts
|
|
5391
|
-
import { readdirSync as readdirSync13, readFileSync as readFileSync20, statSync as
|
|
5510
|
+
import { readdirSync as readdirSync13, readFileSync as readFileSync20, statSync as statSync8 } from "fs";
|
|
5392
5511
|
import { join as join15 } from "path";
|
|
5393
5512
|
function localeFromLproj2(dir) {
|
|
5394
5513
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
@@ -5534,7 +5653,7 @@ var init_apple_stringsdict2 = __esm({
|
|
|
5534
5653
|
const file = join15(localeRoot, dir, TABLE2);
|
|
5535
5654
|
let text;
|
|
5536
5655
|
try {
|
|
5537
|
-
if (!
|
|
5656
|
+
if (!statSync8(file).isFile()) continue;
|
|
5538
5657
|
text = readFileSync20(file, "utf8");
|
|
5539
5658
|
} catch {
|
|
5540
5659
|
continue;
|
|
@@ -6619,10 +6738,139 @@ var init_ui_prefs = __esm({
|
|
|
6619
6738
|
}
|
|
6620
6739
|
});
|
|
6621
6740
|
|
|
6741
|
+
// src/server/events.ts
|
|
6742
|
+
function createEventHub() {
|
|
6743
|
+
const senders = /* @__PURE__ */ new Set();
|
|
6744
|
+
return {
|
|
6745
|
+
subscribe(send) {
|
|
6746
|
+
senders.add(send);
|
|
6747
|
+
return () => senders.delete(send);
|
|
6748
|
+
},
|
|
6749
|
+
broadcast(event, data) {
|
|
6750
|
+
for (const send of [...senders]) {
|
|
6751
|
+
try {
|
|
6752
|
+
send(event, data);
|
|
6753
|
+
} catch {
|
|
6754
|
+
}
|
|
6755
|
+
}
|
|
6756
|
+
},
|
|
6757
|
+
size() {
|
|
6758
|
+
return senders.size;
|
|
6759
|
+
}
|
|
6760
|
+
};
|
|
6761
|
+
}
|
|
6762
|
+
var init_events = __esm({
|
|
6763
|
+
"src/server/events.ts"() {
|
|
6764
|
+
"use strict";
|
|
6765
|
+
}
|
|
6766
|
+
});
|
|
6767
|
+
|
|
6768
|
+
// src/server/watch.ts
|
|
6769
|
+
import { statSync as statSync9, readdirSync as readdirSync14 } from "fs";
|
|
6770
|
+
import { join as join17 } from "path";
|
|
6771
|
+
import { createHash as createHash2 } from "crypto";
|
|
6772
|
+
function hashState(state) {
|
|
6773
|
+
return createHash2("sha1").update(serializeJson(state, state.config.format)).digest("hex");
|
|
6774
|
+
}
|
|
6775
|
+
function signature(statePath) {
|
|
6776
|
+
const fmt = detectFormat(statePath);
|
|
6777
|
+
if (fmt === "none") return "none";
|
|
6778
|
+
if (fmt === "single") {
|
|
6779
|
+
const s = statSync9(statePath);
|
|
6780
|
+
return `single:${s.size}:${s.mtimeMs}`;
|
|
6781
|
+
}
|
|
6782
|
+
const dir = splitDirFor(statePath);
|
|
6783
|
+
const parts = [];
|
|
6784
|
+
for (const rel of ["config.json", "keys.json"]) {
|
|
6785
|
+
try {
|
|
6786
|
+
const s = statSync9(join17(dir, rel));
|
|
6787
|
+
parts.push(`${rel}:${s.size}:${s.mtimeMs}`);
|
|
6788
|
+
} catch {
|
|
6789
|
+
}
|
|
6790
|
+
}
|
|
6791
|
+
try {
|
|
6792
|
+
for (const name of readdirSync14(join17(dir, "locales")).sort()) {
|
|
6793
|
+
if (!name.endsWith(".json")) continue;
|
|
6794
|
+
const s = statSync9(join17(dir, "locales", name));
|
|
6795
|
+
parts.push(`${name}:${s.size}:${s.mtimeMs}`);
|
|
6796
|
+
}
|
|
6797
|
+
} catch {
|
|
6798
|
+
}
|
|
6799
|
+
return `split:${parts.join("|")}`;
|
|
6800
|
+
}
|
|
6801
|
+
function createStateWatcher(opts) {
|
|
6802
|
+
const intervalMs = opts.intervalMs ?? 750;
|
|
6803
|
+
let statePath = opts.statePath;
|
|
6804
|
+
let lastSig = "";
|
|
6805
|
+
let lastHash = "";
|
|
6806
|
+
let timer;
|
|
6807
|
+
function baseline() {
|
|
6808
|
+
try {
|
|
6809
|
+
lastSig = signature(statePath);
|
|
6810
|
+
lastHash = hashState(loadState(statePath));
|
|
6811
|
+
} catch {
|
|
6812
|
+
lastSig = "";
|
|
6813
|
+
lastHash = "";
|
|
6814
|
+
}
|
|
6815
|
+
}
|
|
6816
|
+
function check() {
|
|
6817
|
+
let sig;
|
|
6818
|
+
try {
|
|
6819
|
+
sig = signature(statePath);
|
|
6820
|
+
} catch {
|
|
6821
|
+
return;
|
|
6822
|
+
}
|
|
6823
|
+
if (sig === lastSig) return;
|
|
6824
|
+
let hash;
|
|
6825
|
+
try {
|
|
6826
|
+
hash = hashState(loadState(statePath));
|
|
6827
|
+
} catch {
|
|
6828
|
+
lastSig = sig;
|
|
6829
|
+
return;
|
|
6830
|
+
}
|
|
6831
|
+
lastSig = sig;
|
|
6832
|
+
if (hash !== lastHash) {
|
|
6833
|
+
lastHash = hash;
|
|
6834
|
+
opts.onChange();
|
|
6835
|
+
}
|
|
6836
|
+
}
|
|
6837
|
+
function noteWrite(state) {
|
|
6838
|
+
try {
|
|
6839
|
+
lastSig = signature(statePath);
|
|
6840
|
+
} catch {
|
|
6841
|
+
lastSig = "";
|
|
6842
|
+
}
|
|
6843
|
+
lastHash = hashState(state);
|
|
6844
|
+
}
|
|
6845
|
+
function retarget(next) {
|
|
6846
|
+
statePath = next;
|
|
6847
|
+
baseline();
|
|
6848
|
+
}
|
|
6849
|
+
function start() {
|
|
6850
|
+
if (timer) return;
|
|
6851
|
+
timer = setInterval(check, intervalMs);
|
|
6852
|
+
timer.unref?.();
|
|
6853
|
+
}
|
|
6854
|
+
function stop() {
|
|
6855
|
+
if (timer) clearInterval(timer);
|
|
6856
|
+
timer = void 0;
|
|
6857
|
+
}
|
|
6858
|
+
baseline();
|
|
6859
|
+
return { check, noteWrite, retarget, start, stop };
|
|
6860
|
+
}
|
|
6861
|
+
var init_watch = __esm({
|
|
6862
|
+
"src/server/watch.ts"() {
|
|
6863
|
+
"use strict";
|
|
6864
|
+
init_state();
|
|
6865
|
+
init_format();
|
|
6866
|
+
init_storage();
|
|
6867
|
+
}
|
|
6868
|
+
});
|
|
6869
|
+
|
|
6622
6870
|
// src/server/api.ts
|
|
6623
6871
|
import { Hono } from "hono";
|
|
6624
6872
|
import { streamSSE } from "hono/streaming";
|
|
6625
|
-
import { readFileSync as readFileSync23, existsSync as existsSync13, readdirSync as
|
|
6873
|
+
import { readFileSync as readFileSync23, existsSync as existsSync13, readdirSync as readdirSync15, statSync as statSync10, rmSync as rmSync6 } from "fs";
|
|
6626
6874
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
6627
6875
|
function projectName(root) {
|
|
6628
6876
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
@@ -6654,6 +6902,14 @@ function createApi(deps) {
|
|
|
6654
6902
|
const app = new Hono();
|
|
6655
6903
|
const load = () => loadState(deps.statePath);
|
|
6656
6904
|
const projectRoot = dirname3(resolve9(deps.statePath));
|
|
6905
|
+
const hub = deps.eventHub ?? createEventHub();
|
|
6906
|
+
const watcher = createStateWatcher({
|
|
6907
|
+
statePath: deps.statePath,
|
|
6908
|
+
intervalMs: deps.watchIntervalMs,
|
|
6909
|
+
onChange: () => hub.broadcast("state-changed", JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString() }))
|
|
6910
|
+
});
|
|
6911
|
+
deps.onWatcher?.(watcher);
|
|
6912
|
+
if (deps.watch) watcher.start();
|
|
6657
6913
|
let translateQueue = Promise.resolve();
|
|
6658
6914
|
const withTranslateLock = (fn) => {
|
|
6659
6915
|
const next = translateQueue.then(fn, fn);
|
|
@@ -6675,12 +6931,30 @@ function createApi(deps) {
|
|
|
6675
6931
|
};
|
|
6676
6932
|
const persist = (s) => {
|
|
6677
6933
|
saveState(deps.statePath, s);
|
|
6934
|
+
watcher.noteWrite(s);
|
|
6678
6935
|
scheduleAutoExport(s);
|
|
6679
6936
|
};
|
|
6680
6937
|
const logChange = (entry) => appendLog(projectRoot, { ...entry, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
6681
6938
|
const valueText = (s, key, locale) => s.keys[key]?.values[locale]?.value;
|
|
6682
6939
|
const uiPrefsPath = deps.uiPrefsPath ?? defaultUiPrefsPath();
|
|
6683
6940
|
app.get("/state", (c) => c.json(load()));
|
|
6941
|
+
app.get("/events", (c) => streamSSE(c, async (stream) => {
|
|
6942
|
+
const send = (event, data) => {
|
|
6943
|
+
void stream.writeSSE({ event, data });
|
|
6944
|
+
};
|
|
6945
|
+
const unsubscribe = hub.subscribe(send);
|
|
6946
|
+
stream.onAbort(unsubscribe);
|
|
6947
|
+
await stream.writeSSE({ event: "ready", data: "" });
|
|
6948
|
+
try {
|
|
6949
|
+
while (!stream.aborted) {
|
|
6950
|
+
await stream.sleep(3e4);
|
|
6951
|
+
if (stream.aborted) break;
|
|
6952
|
+
await stream.writeSSE({ event: "ping", data: "" });
|
|
6953
|
+
}
|
|
6954
|
+
} finally {
|
|
6955
|
+
unsubscribe();
|
|
6956
|
+
}
|
|
6957
|
+
}));
|
|
6684
6958
|
app.get("/ui-prefs", (c) => c.json(loadUiPrefs(uiPrefsPath)));
|
|
6685
6959
|
app.put("/ui-prefs", async (c) => {
|
|
6686
6960
|
const body = await c.req.json();
|
|
@@ -6768,7 +7042,7 @@ function createApi(deps) {
|
|
|
6768
7042
|
if (depth > 4) return;
|
|
6769
7043
|
let entries = [];
|
|
6770
7044
|
try {
|
|
6771
|
-
entries =
|
|
7045
|
+
entries = readdirSync15(dir);
|
|
6772
7046
|
} catch {
|
|
6773
7047
|
return;
|
|
6774
7048
|
}
|
|
@@ -6782,7 +7056,7 @@ function createApi(deps) {
|
|
|
6782
7056
|
filePath = abs;
|
|
6783
7057
|
} else {
|
|
6784
7058
|
try {
|
|
6785
|
-
if (
|
|
7059
|
+
if (statSync10(abs).isDirectory()) walk(abs, depth + 1);
|
|
6786
7060
|
} catch {
|
|
6787
7061
|
}
|
|
6788
7062
|
continue;
|
|
@@ -6813,6 +7087,7 @@ function createApi(deps) {
|
|
|
6813
7087
|
if (!existsSync13(resolved)) return c.json({ error: "file not found" }, 400);
|
|
6814
7088
|
loadState(resolved);
|
|
6815
7089
|
deps.statePath = resolved;
|
|
7090
|
+
watcher.retarget(resolved);
|
|
6816
7091
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
6817
7092
|
});
|
|
6818
7093
|
app.post("/keys", async (c) => {
|
|
@@ -7816,6 +8091,8 @@ var init_api = __esm({
|
|
|
7816
8091
|
init_ui_prefs();
|
|
7817
8092
|
init_local_settings();
|
|
7818
8093
|
init_atomic_write();
|
|
8094
|
+
init_events();
|
|
8095
|
+
init_watch();
|
|
7819
8096
|
sanitize = (s) => s.replace(/[^\w.\-]+/g, "_");
|
|
7820
8097
|
screenshotDirName = (statePath) => basename(statePath).replace(/\.[^.]+$/, "") + "-screenshots";
|
|
7821
8098
|
}
|
|
@@ -7830,7 +8107,7 @@ __export(server_exports, {
|
|
|
7830
8107
|
import { Hono as Hono2 } from "hono";
|
|
7831
8108
|
import { serve } from "@hono/node-server";
|
|
7832
8109
|
import { fileURLToPath } from "url";
|
|
7833
|
-
import { dirname as dirname4, join as
|
|
8110
|
+
import { dirname as dirname4, join as join18, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
7834
8111
|
import { readFile, stat } from "fs/promises";
|
|
7835
8112
|
import { createServer } from "net";
|
|
7836
8113
|
import open from "open";
|
|
@@ -7845,9 +8122,22 @@ async function readFileResponse(absPath) {
|
|
|
7845
8122
|
return null;
|
|
7846
8123
|
}
|
|
7847
8124
|
}
|
|
8125
|
+
function isLocalHost(hostname) {
|
|
8126
|
+
const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
8127
|
+
return h === "localhost" || h === "127.0.0.1" || h === "::1" || h.endsWith(".localhost");
|
|
8128
|
+
}
|
|
7848
8129
|
function buildApp(opts) {
|
|
7849
8130
|
const app = new Hono2();
|
|
7850
|
-
|
|
8131
|
+
app.use("*", async (c, next) => {
|
|
8132
|
+
if (!isLocalHost(new URL(c.req.url).hostname)) return c.text("Forbidden: non-local Host header", 403);
|
|
8133
|
+
return next();
|
|
8134
|
+
});
|
|
8135
|
+
const apiDeps = {
|
|
8136
|
+
statePath: opts.statePath,
|
|
8137
|
+
autoExport: true,
|
|
8138
|
+
watch: opts.watch,
|
|
8139
|
+
onWatcher: opts.onWatcher
|
|
8140
|
+
};
|
|
7851
8141
|
app.route("/api", createApi(apiDeps));
|
|
7852
8142
|
app.get("/:dir/*", async (c, next) => {
|
|
7853
8143
|
const dirSeg = c.req.param("dir");
|
|
@@ -7873,7 +8163,7 @@ function buildApp(opts) {
|
|
|
7873
8163
|
const file = await readFileResponse(target);
|
|
7874
8164
|
if (file) return file;
|
|
7875
8165
|
}
|
|
7876
|
-
const index = await readFileResponse(
|
|
8166
|
+
const index = await readFileResponse(join18(root, "index.html"));
|
|
7877
8167
|
if (index) return index;
|
|
7878
8168
|
return c.notFound();
|
|
7879
8169
|
});
|
|
@@ -7898,13 +8188,19 @@ function findAvailablePort(start) {
|
|
|
7898
8188
|
});
|
|
7899
8189
|
}
|
|
7900
8190
|
async function startServer(opts) {
|
|
7901
|
-
|
|
8191
|
+
let watcher;
|
|
8192
|
+
const app = buildApp({ ...opts, watch: opts.watch ?? true, onWatcher: (w) => {
|
|
8193
|
+
watcher = w;
|
|
8194
|
+
} });
|
|
7902
8195
|
const port = await findAvailablePort(opts.dev ? DEV_PORT : DEFAULT_PORT);
|
|
7903
8196
|
return new Promise((resolveP) => {
|
|
7904
8197
|
const server = serve({ fetch: app.fetch, hostname: "127.0.0.1", port }, (info) => {
|
|
7905
8198
|
const url = `http://127.0.0.1:${info.port}`;
|
|
7906
8199
|
if (opts.open !== false && !opts.dev) void open(url);
|
|
7907
|
-
resolveP({ url, close: () =>
|
|
8200
|
+
resolveP({ url, close: () => {
|
|
8201
|
+
watcher?.stop();
|
|
8202
|
+
server.close();
|
|
8203
|
+
} });
|
|
7908
8204
|
backgroundScan(opts.statePath);
|
|
7909
8205
|
});
|
|
7910
8206
|
});
|
|
@@ -7937,7 +8233,7 @@ var init_server = __esm({
|
|
|
7937
8233
|
init_scanner();
|
|
7938
8234
|
init_usage();
|
|
7939
8235
|
here = dirname4(fileURLToPath(import.meta.url));
|
|
7940
|
-
DEFAULT_UI_DIR =
|
|
8236
|
+
DEFAULT_UI_DIR = join18(here, "..", "ui");
|
|
7941
8237
|
MIME = {
|
|
7942
8238
|
".html": "text/html; charset=utf-8",
|
|
7943
8239
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -7987,7 +8283,7 @@ init_usage();
|
|
|
7987
8283
|
init_context();
|
|
7988
8284
|
init_run2();
|
|
7989
8285
|
init_outputs();
|
|
7990
|
-
import { resolve as resolve11, dirname as dirname5, join as
|
|
8286
|
+
import { resolve as resolve11, dirname as dirname5, join as join19, basename as basename2 } from "path";
|
|
7991
8287
|
import { readFileSync as readFileSync24, existsSync as existsSync14, mkdirSync as mkdirSync6, cpSync } from "fs";
|
|
7992
8288
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7993
8289
|
|
|
@@ -8028,7 +8324,7 @@ function formatText(report) {
|
|
|
8028
8324
|
function formatJson(report) {
|
|
8029
8325
|
return JSON.stringify(report, null, 2) + "\n";
|
|
8030
8326
|
}
|
|
8031
|
-
function formatSarif(report,
|
|
8327
|
+
function formatSarif(report, ctx) {
|
|
8032
8328
|
const ruleIds = [...new Set(report.findings.map((f) => f.ruleId))];
|
|
8033
8329
|
const sarif = {
|
|
8034
8330
|
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
@@ -8036,14 +8332,24 @@ function formatSarif(report, rawText) {
|
|
|
8036
8332
|
runs: [{
|
|
8037
8333
|
tool: { driver: { name: "glotfile", rules: ruleIds.map((id) => ({ id })) } },
|
|
8038
8334
|
results: report.findings.map((f) => {
|
|
8039
|
-
const
|
|
8040
|
-
return {
|
|
8335
|
+
const base = {
|
|
8041
8336
|
ruleId: f.ruleId,
|
|
8042
|
-
level: f.severity === "error" ? "error" : "warning"
|
|
8337
|
+
level: f.severity === "error" ? "error" : "warning"
|
|
8338
|
+
};
|
|
8339
|
+
if (f.ruleId === "output-stale") {
|
|
8340
|
+
return {
|
|
8341
|
+
...base,
|
|
8342
|
+
message: { text: `${f.key}: ${f.message}` },
|
|
8343
|
+
locations: [{ physicalLocation: { artifactLocation: { uri: f.key } } }]
|
|
8344
|
+
};
|
|
8345
|
+
}
|
|
8346
|
+
const pos = locate(ctx.keysRawText, f.key);
|
|
8347
|
+
return {
|
|
8348
|
+
...base,
|
|
8043
8349
|
message: { text: `${f.key}${f.locale ? ` [${f.locale}]` : ""}: ${f.message}` },
|
|
8044
8350
|
locations: [{
|
|
8045
8351
|
physicalLocation: {
|
|
8046
|
-
artifactLocation: { uri:
|
|
8352
|
+
artifactLocation: { uri: ctx.keysUri },
|
|
8047
8353
|
region: { startLine: pos.line, startColumn: pos.column }
|
|
8048
8354
|
}
|
|
8049
8355
|
}]
|
|
@@ -8458,9 +8764,23 @@ async function runContextBatchAction(args, pending, action, projectRoot) {
|
|
|
8458
8764
|
if (outcome.retried) console.log(`${outcome.retried} job(s) re-run synchronously (batch entries failed or were malformed).`);
|
|
8459
8765
|
for (const e of outcome.errors) console.warn(`skip ${e.key}: ${e.error}`);
|
|
8460
8766
|
}
|
|
8461
|
-
function
|
|
8767
|
+
function sarifContextFor(statePath) {
|
|
8768
|
+
if (detectFormat(statePath) === "split") {
|
|
8769
|
+
const dir = splitDirFor(statePath);
|
|
8770
|
+
const keysPath = join19(dir, "keys.json");
|
|
8771
|
+
return {
|
|
8772
|
+
keysUri: `${basename2(dir)}/keys.json`,
|
|
8773
|
+
keysRawText: existsSync14(keysPath) ? readFileSync24(keysPath, "utf8") : ""
|
|
8774
|
+
};
|
|
8775
|
+
}
|
|
8776
|
+
return {
|
|
8777
|
+
keysUri: basename2(statePath),
|
|
8778
|
+
keysRawText: existsSync14(statePath) ? readFileSync24(statePath, "utf8") : ""
|
|
8779
|
+
};
|
|
8780
|
+
}
|
|
8781
|
+
function printReport(report, format, statePath) {
|
|
8462
8782
|
if (format === "json") console.log(formatJson(report).trimEnd());
|
|
8463
|
-
else if (format === "sarif") console.log(formatSarif(report,
|
|
8783
|
+
else if (format === "sarif") console.log(formatSarif(report, sarifContextFor(statePath)).trimEnd());
|
|
8464
8784
|
else console.log(formatText(report).trimEnd());
|
|
8465
8785
|
}
|
|
8466
8786
|
async function runLintCmd(args) {
|
|
@@ -8477,13 +8797,12 @@ async function runLintCmd(args) {
|
|
|
8477
8797
|
}
|
|
8478
8798
|
return;
|
|
8479
8799
|
}
|
|
8480
|
-
const rawText = existsSync14(args.statePath) ? readFileSync24(args.statePath, "utf8") : "";
|
|
8481
8800
|
const report = await runLint(state, {
|
|
8482
8801
|
locales: args.locales,
|
|
8483
8802
|
ruleIds: args.ruleIds,
|
|
8484
8803
|
includeSuppressed: args.includeSuppressed
|
|
8485
8804
|
});
|
|
8486
|
-
printReport(report, args.format,
|
|
8805
|
+
printReport(report, args.format, args.statePath);
|
|
8487
8806
|
const tooManyWarnings = args.maxWarnings != null && report.counts.warn > args.maxWarnings;
|
|
8488
8807
|
if (!report.ok || tooManyWarnings) process.exitCode = 1;
|
|
8489
8808
|
}
|
|
@@ -8497,17 +8816,16 @@ async function runCheck(args) {
|
|
|
8497
8816
|
counts: { error: 1, warn: 0, suppressed: 0 },
|
|
8498
8817
|
ok: false
|
|
8499
8818
|
};
|
|
8500
|
-
printReport(report2, args.format,
|
|
8819
|
+
printReport(report2, args.format, args.statePath);
|
|
8501
8820
|
process.exitCode = 1;
|
|
8502
8821
|
return;
|
|
8503
8822
|
}
|
|
8504
|
-
const rawText = existsSync14(args.statePath) ? readFileSync24(args.statePath, "utf8") : "";
|
|
8505
8823
|
const root = dirname5(resolve11(args.statePath));
|
|
8506
8824
|
const lint = await runLint(state, {});
|
|
8507
8825
|
const findings = sortFindings([...lint.findings, ...checkOutputs(state, root)]);
|
|
8508
8826
|
const counts = { ...countSeverities(findings), suppressed: lint.counts.suppressed };
|
|
8509
8827
|
const report = { findings, counts, ok: counts.error === 0 };
|
|
8510
|
-
printReport(report, args.format,
|
|
8828
|
+
printReport(report, args.format, args.statePath);
|
|
8511
8829
|
if (!report.ok) process.exitCode = 1;
|
|
8512
8830
|
}
|
|
8513
8831
|
async function runImportCmd(args) {
|
|
@@ -8708,6 +9026,7 @@ async function runPrune(args) {
|
|
|
8708
9026
|
}
|
|
8709
9027
|
const state = loadState(args.statePath);
|
|
8710
9028
|
const toRemove = /* @__PURE__ */ new Set();
|
|
9029
|
+
let heuristicUnused = false;
|
|
8711
9030
|
if (args.emptySource) {
|
|
8712
9031
|
for (const k of findEmptySourceKeys(state)) toRemove.add(k);
|
|
8713
9032
|
}
|
|
@@ -8723,6 +9042,7 @@ async function runPrune(args) {
|
|
|
8723
9042
|
for (const k of Object.keys(state.keys)) {
|
|
8724
9043
|
if (!used.has(k)) toRemove.add(k);
|
|
8725
9044
|
}
|
|
9045
|
+
heuristicUnused = true;
|
|
8726
9046
|
}
|
|
8727
9047
|
}
|
|
8728
9048
|
const keys = [...toRemove].sort();
|
|
@@ -8730,6 +9050,11 @@ async function runPrune(args) {
|
|
|
8730
9050
|
console.log("No keys to prune.");
|
|
8731
9051
|
return;
|
|
8732
9052
|
}
|
|
9053
|
+
if (heuristicUnused) {
|
|
9054
|
+
console.warn(
|
|
9055
|
+
"Note: --unused is heuristic \u2014 keys used via an unrecognised wrapper or a fully dynamic key can appear here by mistake. Review the list and add a `scan.keep` glob for any false positive."
|
|
9056
|
+
);
|
|
9057
|
+
}
|
|
8733
9058
|
if (!args.write) {
|
|
8734
9059
|
for (const k of keys) console.log(k);
|
|
8735
9060
|
console.log(`${keys.length} key(s) to prune. Run with --write to remove them.`);
|
|
@@ -8751,10 +9076,10 @@ function runSplit(args) {
|
|
|
8751
9076
|
`Split catalog into ${splitDirFor(args.statePath)}/ (config.json, keys.json, locales/ \u2014 up to ${state.config.locales.length} locale files). Removed ${args.statePath}.`
|
|
8752
9077
|
);
|
|
8753
9078
|
}
|
|
8754
|
-
var SKILL_SRC =
|
|
9079
|
+
var SKILL_SRC = join19(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "skill");
|
|
8755
9080
|
function runSkill(args) {
|
|
8756
9081
|
if (args.print) {
|
|
8757
|
-
console.log(readFileSync24(
|
|
9082
|
+
console.log(readFileSync24(join19(SKILL_SRC, "SKILL.md"), "utf8").trimEnd());
|
|
8758
9083
|
return;
|
|
8759
9084
|
}
|
|
8760
9085
|
const dest = resolve11(process.cwd(), ".claude", "skills", "glotfile");
|
|
@@ -8916,7 +9241,7 @@ ${formatOpts([...options, ...GLOBAL_OPTS])}`);
|
|
|
8916
9241
|
);
|
|
8917
9242
|
}
|
|
8918
9243
|
function printVersion() {
|
|
8919
|
-
const pkgPath =
|
|
9244
|
+
const pkgPath = join19(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
|
|
8920
9245
|
console.log(JSON.parse(readFileSync24(pkgPath, "utf8")).version);
|
|
8921
9246
|
}
|
|
8922
9247
|
async function main(argv) {
|