glotfile 0.8.2 → 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
|
}
|
|
@@ -2465,6 +2476,7 @@ var init_anthropic = __esm({
|
|
|
2465
2476
|
}, { signal });
|
|
2466
2477
|
this.recordUsage(res.usage);
|
|
2467
2478
|
const text = res.content.find((b) => b.type === "text")?.text ?? "";
|
|
2479
|
+
if (res.stop_reason === "max_tokens") throw new MalformedReplyError(text);
|
|
2468
2480
|
return parseReplyItems(text);
|
|
2469
2481
|
}
|
|
2470
2482
|
};
|
|
@@ -2558,6 +2570,7 @@ var init_openai = __esm({
|
|
|
2558
2570
|
]
|
|
2559
2571
|
}, { signal });
|
|
2560
2572
|
const text = res.choices?.[0]?.message?.content ?? "";
|
|
2573
|
+
if (res.choices?.[0]?.finish_reason === "length") throw new MalformedReplyError(text);
|
|
2561
2574
|
return parseReplyItems(text);
|
|
2562
2575
|
}
|
|
2563
2576
|
};
|
|
@@ -2675,8 +2688,11 @@ var init_bedrock = __esm({
|
|
|
2675
2688
|
const res = await this.client.send(this.makeCommand(this.buildInput(batch)), { abortSignal: signal });
|
|
2676
2689
|
const blocks = res.output?.message?.content ?? [];
|
|
2677
2690
|
const tool = blocks.find((b) => b.toolUse)?.toolUse;
|
|
2678
|
-
if (tool?.input?.items) return tool.input.items;
|
|
2679
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;
|
|
2680
2696
|
return parseReplyItems(text);
|
|
2681
2697
|
}
|
|
2682
2698
|
};
|
|
@@ -3132,8 +3148,30 @@ function attachScreenshotsForProvider(reqs, state, projectRoot, supportsVision)
|
|
|
3132
3148
|
const keys = new Set(reqs.filter((r) => state.keys[r.key]?.screenshot).map((r) => r.key));
|
|
3133
3149
|
return { skipped: keys.size };
|
|
3134
3150
|
}
|
|
3135
|
-
|
|
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 = {}) {
|
|
3136
3172
|
if (!reqs.length) return [];
|
|
3173
|
+
const maxRetries = retry.retries ?? 3;
|
|
3174
|
+
const delayMs = retry.delayMs ?? ((attempt) => 250 * 2 ** attempt);
|
|
3137
3175
|
const byLocale = /* @__PURE__ */ new Map();
|
|
3138
3176
|
for (const req of reqs) {
|
|
3139
3177
|
let group = byLocale.get(req.targetLocale);
|
|
@@ -3168,10 +3206,20 @@ async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAU
|
|
|
3168
3206
|
started.add(locale);
|
|
3169
3207
|
hooks.onLocaleStart?.(locale);
|
|
3170
3208
|
}
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
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
|
+
}
|
|
3175
3223
|
allResults.push(...batchResults);
|
|
3176
3224
|
const left = remaining.get(locale) - 1;
|
|
3177
3225
|
remaining.set(locale, left);
|
|
@@ -3262,26 +3310,63 @@ var init_pending_batch = __esm({
|
|
|
3262
3310
|
});
|
|
3263
3311
|
|
|
3264
3312
|
// src/server/log.ts
|
|
3265
|
-
import { appendFileSync,
|
|
3313
|
+
import { appendFileSync, existsSync as existsSync7, openSync, fstatSync, readSync, closeSync, statSync as statSync2, readFileSync as readFileSync7 } from "fs";
|
|
3266
3314
|
import { resolve as resolve5 } from "path";
|
|
3267
3315
|
function logPath(projectRoot) {
|
|
3268
3316
|
return resolve5(projectRoot, ".glotfile", "log.jsonl");
|
|
3269
3317
|
}
|
|
3270
3318
|
function appendLog(projectRoot, entry) {
|
|
3271
3319
|
ensureGlotfileDir(projectRoot);
|
|
3272
|
-
appendFileSync(logPath(projectRoot), JSON.stringify(entry) + "\n", "utf8");
|
|
3273
|
-
}
|
|
3274
|
-
function readLog(projectRoot, limit = 100) {
|
|
3275
3320
|
const path = logPath(projectRoot);
|
|
3276
|
-
|
|
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;
|
|
3277
3326
|
const lines = readFileSync7(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
3278
|
-
const
|
|
3279
|
-
|
|
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
|
+
}
|
|
3280
3358
|
}
|
|
3359
|
+
function readLog(projectRoot, limit = 100) {
|
|
3360
|
+
return readLastLines(logPath(projectRoot), limit).map((l) => JSON.parse(l)).reverse();
|
|
3361
|
+
}
|
|
3362
|
+
var MAX_LOG_BYTES, TRIM_LOG_TO_BYTES;
|
|
3281
3363
|
var init_log = __esm({
|
|
3282
3364
|
"src/server/log.ts"() {
|
|
3283
3365
|
"use strict";
|
|
3284
3366
|
init_glotfile_dir();
|
|
3367
|
+
init_atomic_write();
|
|
3368
|
+
MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
3369
|
+
TRIM_LOG_TO_BYTES = 4 * 1024 * 1024;
|
|
3285
3370
|
}
|
|
3286
3371
|
});
|
|
3287
3372
|
|
|
@@ -3873,7 +3958,7 @@ var init_scan = __esm({
|
|
|
3873
3958
|
});
|
|
3874
3959
|
|
|
3875
3960
|
// src/server/scanner.ts
|
|
3876
|
-
import { readdirSync as readdirSync3, statSync as
|
|
3961
|
+
import { readdirSync as readdirSync3, statSync as statSync3, readFileSync as readFileSync11 } from "fs";
|
|
3877
3962
|
import { join as join5, extname as extname2, relative } from "path";
|
|
3878
3963
|
function scannerForExt(ext) {
|
|
3879
3964
|
return EXT_SCANNER[ext] ?? null;
|
|
@@ -4030,7 +4115,7 @@ function* walkFiles(dir, root, exclude) {
|
|
|
4030
4115
|
const rel = relative(root, abs);
|
|
4031
4116
|
let st;
|
|
4032
4117
|
try {
|
|
4033
|
-
st =
|
|
4118
|
+
st = statSync3(abs);
|
|
4034
4119
|
} catch {
|
|
4035
4120
|
continue;
|
|
4036
4121
|
}
|
|
@@ -4059,7 +4144,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
4059
4144
|
const abs = join5(projectRoot, relPath);
|
|
4060
4145
|
let st;
|
|
4061
4146
|
try {
|
|
4062
|
-
st =
|
|
4147
|
+
st = statSync3(abs);
|
|
4063
4148
|
} catch {
|
|
4064
4149
|
continue;
|
|
4065
4150
|
}
|
|
@@ -4110,7 +4195,21 @@ var init_scanner = __esm({
|
|
|
4110
4195
|
// t('key') — word boundary before t, not preceded by dot (excludes i18n.t which is above)
|
|
4111
4196
|
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*'([^']+)'/g,
|
|
4112
4197
|
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*"([^"]+)"/g,
|
|
4113
|
-
/(?<!\.)(?<![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
|
|
4114
4213
|
],
|
|
4115
4214
|
gettext: [
|
|
4116
4215
|
/\b(?:gettext|ngettext)\s*\(\s*'([^']+)'/g,
|
|
@@ -4144,7 +4243,7 @@ var init_scanner = __esm({
|
|
|
4144
4243
|
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$]*)\$\{/g
|
|
4145
4244
|
]
|
|
4146
4245
|
};
|
|
4147
|
-
CACHE_VERSION =
|
|
4246
|
+
CACHE_VERSION = 7;
|
|
4148
4247
|
EXT_SCANNER = {
|
|
4149
4248
|
".php": "laravel",
|
|
4150
4249
|
".vue": "js-i18n",
|
|
@@ -4188,11 +4287,11 @@ var init_scanner = __esm({
|
|
|
4188
4287
|
});
|
|
4189
4288
|
|
|
4190
4289
|
// src/server/import/detect.ts
|
|
4191
|
-
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";
|
|
4192
4291
|
import { join as join6 } from "path";
|
|
4193
4292
|
function safeIsDir(p) {
|
|
4194
4293
|
try {
|
|
4195
|
-
return
|
|
4294
|
+
return statSync4(p).isDirectory();
|
|
4196
4295
|
} catch {
|
|
4197
4296
|
return false;
|
|
4198
4297
|
}
|
|
@@ -4228,7 +4327,7 @@ function detectVue(root, forced = false) {
|
|
|
4228
4327
|
if (enough) {
|
|
4229
4328
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4230
4329
|
try {
|
|
4231
|
-
return
|
|
4330
|
+
return statSync4(join6(localeRoot, `${loc}.json`)).size;
|
|
4232
4331
|
} catch {
|
|
4233
4332
|
return 0;
|
|
4234
4333
|
}
|
|
@@ -4265,7 +4364,7 @@ function detectApple(root) {
|
|
|
4265
4364
|
locales,
|
|
4266
4365
|
sourceLocale: pickSource(locales, (loc) => {
|
|
4267
4366
|
try {
|
|
4268
|
-
return
|
|
4367
|
+
return statSync4(join6(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
4269
4368
|
} catch {
|
|
4270
4369
|
return 0;
|
|
4271
4370
|
}
|
|
@@ -4325,7 +4424,7 @@ function detectI18next(root) {
|
|
|
4325
4424
|
if (locales.length === 0) continue;
|
|
4326
4425
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4327
4426
|
try {
|
|
4328
|
-
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);
|
|
4329
4428
|
} catch {
|
|
4330
4429
|
return 0;
|
|
4331
4430
|
}
|
|
@@ -4430,10 +4529,9 @@ var init_detect = __esm({
|
|
|
4430
4529
|
function flattenObject(value, prefix, warnings) {
|
|
4431
4530
|
const out = {};
|
|
4432
4531
|
const walk = (node, path) => {
|
|
4433
|
-
if (typeof node === "string") {
|
|
4434
|
-
out
|
|
4435
|
-
|
|
4436
|
-
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);
|
|
4437
4535
|
} else if (Array.isArray(node)) {
|
|
4438
4536
|
node.forEach((el, i) => walk(el, path ? `${path}.${i}` : String(i)));
|
|
4439
4537
|
} else if (node && typeof node === "object") {
|
|
@@ -4502,18 +4600,18 @@ var init_placeholders2 = __esm({
|
|
|
4502
4600
|
});
|
|
4503
4601
|
|
|
4504
4602
|
// src/server/import/parsers/laravel-php.ts
|
|
4505
|
-
import { readdirSync as readdirSync6, statSync as
|
|
4603
|
+
import { readdirSync as readdirSync6, statSync as statSync5 } from "fs";
|
|
4506
4604
|
import { join as join8, relative as relative2 } from "path";
|
|
4507
4605
|
import { execFileSync } from "child_process";
|
|
4508
4606
|
function listDirs2(dir) {
|
|
4509
|
-
return readdirSync6(dir).filter((e) =>
|
|
4607
|
+
return readdirSync6(dir).filter((e) => statSync5(join8(dir, e)).isDirectory());
|
|
4510
4608
|
}
|
|
4511
4609
|
function listPhpFiles(dir) {
|
|
4512
4610
|
const out = [];
|
|
4513
4611
|
const walk = (d) => {
|
|
4514
4612
|
for (const e of readdirSync6(d)) {
|
|
4515
4613
|
const full = join8(d, e);
|
|
4516
|
-
if (
|
|
4614
|
+
if (statSync5(full).isDirectory()) walk(full);
|
|
4517
4615
|
else if (e.endsWith(".php")) out.push(full);
|
|
4518
4616
|
}
|
|
4519
4617
|
};
|
|
@@ -4648,7 +4746,7 @@ var init_flutter_arb2 = __esm({
|
|
|
4648
4746
|
});
|
|
4649
4747
|
|
|
4650
4748
|
// src/server/import/parsers/apple-strings.ts
|
|
4651
|
-
import { readdirSync as readdirSync8, readFileSync as readFileSync15, statSync as
|
|
4749
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync15, statSync as statSync6 } from "fs";
|
|
4652
4750
|
import { join as join10 } from "path";
|
|
4653
4751
|
function localeFromLproj(dir) {
|
|
4654
4752
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
@@ -4715,25 +4813,34 @@ function parseStrings(text, file, warnings) {
|
|
|
4715
4813
|
while (i < n && !/[\s=;]/.test(text[i])) raw += text[i++];
|
|
4716
4814
|
return raw.length ? raw : null;
|
|
4717
4815
|
};
|
|
4816
|
+
const recover = () => {
|
|
4817
|
+
while (i < n && text[i] !== ";") i++;
|
|
4818
|
+
if (i >= n) return false;
|
|
4819
|
+
i++;
|
|
4820
|
+
return true;
|
|
4821
|
+
};
|
|
4718
4822
|
while (true) {
|
|
4719
4823
|
skipTrivia();
|
|
4720
4824
|
if (i >= n) break;
|
|
4721
4825
|
const key = readToken();
|
|
4722
4826
|
if (key === null) {
|
|
4723
4827
|
warnings.push(`apple-strings: malformed entry in ${file} near offset ${i}`);
|
|
4724
|
-
break;
|
|
4828
|
+
if (!recover()) break;
|
|
4829
|
+
continue;
|
|
4725
4830
|
}
|
|
4726
4831
|
skipTrivia();
|
|
4727
4832
|
if (text[i] !== "=") {
|
|
4728
4833
|
warnings.push(`apple-strings: expected '=' after key "${key}" in ${file}`);
|
|
4729
|
-
break;
|
|
4834
|
+
if (!recover()) break;
|
|
4835
|
+
continue;
|
|
4730
4836
|
}
|
|
4731
4837
|
i++;
|
|
4732
4838
|
skipTrivia();
|
|
4733
4839
|
const value = readToken();
|
|
4734
4840
|
if (value === null) {
|
|
4735
4841
|
warnings.push(`apple-strings: missing value for key "${key}" in ${file}`);
|
|
4736
|
-
break;
|
|
4842
|
+
if (!recover()) break;
|
|
4843
|
+
continue;
|
|
4737
4844
|
}
|
|
4738
4845
|
skipTrivia();
|
|
4739
4846
|
if (text[i] === ";") i++;
|
|
@@ -4760,7 +4867,7 @@ var init_apple_strings2 = __esm({
|
|
|
4760
4867
|
const file = join10(localeRoot, dir, TABLE);
|
|
4761
4868
|
let text;
|
|
4762
4869
|
try {
|
|
4763
|
-
if (!
|
|
4870
|
+
if (!statSync6(file).isFile()) continue;
|
|
4764
4871
|
text = readFileSync15(file, "utf8");
|
|
4765
4872
|
} catch {
|
|
4766
4873
|
continue;
|
|
@@ -5053,11 +5160,11 @@ var init_gettext_po2 = __esm({
|
|
|
5053
5160
|
});
|
|
5054
5161
|
|
|
5055
5162
|
// src/server/import/parsers/i18next-json.ts
|
|
5056
|
-
import { readdirSync as readdirSync11, readFileSync as readFileSync18, statSync as
|
|
5163
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync18, statSync as statSync7 } from "fs";
|
|
5057
5164
|
import { join as join13 } from "path";
|
|
5058
5165
|
function safeIsDir2(p) {
|
|
5059
5166
|
try {
|
|
5060
|
-
return
|
|
5167
|
+
return statSync7(p).isDirectory();
|
|
5061
5168
|
} catch {
|
|
5062
5169
|
return false;
|
|
5063
5170
|
}
|
|
@@ -5168,6 +5275,15 @@ function decodeDouble(body) {
|
|
|
5168
5275
|
}
|
|
5169
5276
|
const n = body[++i];
|
|
5170
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
|
+
}
|
|
5171
5287
|
out += n === "n" ? "\n" : n === "r" ? "\r" : n === "t" ? " " : n;
|
|
5172
5288
|
}
|
|
5173
5289
|
return out;
|
|
@@ -5391,7 +5507,7 @@ var init_rails_yaml2 = __esm({
|
|
|
5391
5507
|
});
|
|
5392
5508
|
|
|
5393
5509
|
// src/server/import/parsers/apple-stringsdict.ts
|
|
5394
|
-
import { readdirSync as readdirSync13, readFileSync as readFileSync20, statSync as
|
|
5510
|
+
import { readdirSync as readdirSync13, readFileSync as readFileSync20, statSync as statSync8 } from "fs";
|
|
5395
5511
|
import { join as join15 } from "path";
|
|
5396
5512
|
function localeFromLproj2(dir) {
|
|
5397
5513
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
@@ -5537,7 +5653,7 @@ var init_apple_stringsdict2 = __esm({
|
|
|
5537
5653
|
const file = join15(localeRoot, dir, TABLE2);
|
|
5538
5654
|
let text;
|
|
5539
5655
|
try {
|
|
5540
|
-
if (!
|
|
5656
|
+
if (!statSync8(file).isFile()) continue;
|
|
5541
5657
|
text = readFileSync20(file, "utf8");
|
|
5542
5658
|
} catch {
|
|
5543
5659
|
continue;
|
|
@@ -6650,7 +6766,7 @@ var init_events = __esm({
|
|
|
6650
6766
|
});
|
|
6651
6767
|
|
|
6652
6768
|
// src/server/watch.ts
|
|
6653
|
-
import { statSync as
|
|
6769
|
+
import { statSync as statSync9, readdirSync as readdirSync14 } from "fs";
|
|
6654
6770
|
import { join as join17 } from "path";
|
|
6655
6771
|
import { createHash as createHash2 } from "crypto";
|
|
6656
6772
|
function hashState(state) {
|
|
@@ -6660,14 +6776,14 @@ function signature(statePath) {
|
|
|
6660
6776
|
const fmt = detectFormat(statePath);
|
|
6661
6777
|
if (fmt === "none") return "none";
|
|
6662
6778
|
if (fmt === "single") {
|
|
6663
|
-
const s =
|
|
6779
|
+
const s = statSync9(statePath);
|
|
6664
6780
|
return `single:${s.size}:${s.mtimeMs}`;
|
|
6665
6781
|
}
|
|
6666
6782
|
const dir = splitDirFor(statePath);
|
|
6667
6783
|
const parts = [];
|
|
6668
6784
|
for (const rel of ["config.json", "keys.json"]) {
|
|
6669
6785
|
try {
|
|
6670
|
-
const s =
|
|
6786
|
+
const s = statSync9(join17(dir, rel));
|
|
6671
6787
|
parts.push(`${rel}:${s.size}:${s.mtimeMs}`);
|
|
6672
6788
|
} catch {
|
|
6673
6789
|
}
|
|
@@ -6675,7 +6791,7 @@ function signature(statePath) {
|
|
|
6675
6791
|
try {
|
|
6676
6792
|
for (const name of readdirSync14(join17(dir, "locales")).sort()) {
|
|
6677
6793
|
if (!name.endsWith(".json")) continue;
|
|
6678
|
-
const s =
|
|
6794
|
+
const s = statSync9(join17(dir, "locales", name));
|
|
6679
6795
|
parts.push(`${name}:${s.size}:${s.mtimeMs}`);
|
|
6680
6796
|
}
|
|
6681
6797
|
} catch {
|
|
@@ -6754,7 +6870,7 @@ var init_watch = __esm({
|
|
|
6754
6870
|
// src/server/api.ts
|
|
6755
6871
|
import { Hono } from "hono";
|
|
6756
6872
|
import { streamSSE } from "hono/streaming";
|
|
6757
|
-
import { readFileSync as readFileSync23, existsSync as existsSync13, readdirSync as readdirSync15, statSync as
|
|
6873
|
+
import { readFileSync as readFileSync23, existsSync as existsSync13, readdirSync as readdirSync15, statSync as statSync10, rmSync as rmSync6 } from "fs";
|
|
6758
6874
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
6759
6875
|
function projectName(root) {
|
|
6760
6876
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
@@ -6940,7 +7056,7 @@ function createApi(deps) {
|
|
|
6940
7056
|
filePath = abs;
|
|
6941
7057
|
} else {
|
|
6942
7058
|
try {
|
|
6943
|
-
if (
|
|
7059
|
+
if (statSync10(abs).isDirectory()) walk(abs, depth + 1);
|
|
6944
7060
|
} catch {
|
|
6945
7061
|
}
|
|
6946
7062
|
continue;
|
|
@@ -8006,8 +8122,16 @@ async function readFileResponse(absPath) {
|
|
|
8006
8122
|
return null;
|
|
8007
8123
|
}
|
|
8008
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
|
+
}
|
|
8009
8129
|
function buildApp(opts) {
|
|
8010
8130
|
const app = new Hono2();
|
|
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
|
+
});
|
|
8011
8135
|
const apiDeps = {
|
|
8012
8136
|
statePath: opts.statePath,
|
|
8013
8137
|
autoExport: true,
|
|
@@ -8159,7 +8283,7 @@ init_usage();
|
|
|
8159
8283
|
init_context();
|
|
8160
8284
|
init_run2();
|
|
8161
8285
|
init_outputs();
|
|
8162
|
-
import { resolve as resolve11, dirname as dirname5, join as join19 } from "path";
|
|
8286
|
+
import { resolve as resolve11, dirname as dirname5, join as join19, basename as basename2 } from "path";
|
|
8163
8287
|
import { readFileSync as readFileSync24, existsSync as existsSync14, mkdirSync as mkdirSync6, cpSync } from "fs";
|
|
8164
8288
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8165
8289
|
|
|
@@ -8200,7 +8324,7 @@ function formatText(report) {
|
|
|
8200
8324
|
function formatJson(report) {
|
|
8201
8325
|
return JSON.stringify(report, null, 2) + "\n";
|
|
8202
8326
|
}
|
|
8203
|
-
function formatSarif(report,
|
|
8327
|
+
function formatSarif(report, ctx) {
|
|
8204
8328
|
const ruleIds = [...new Set(report.findings.map((f) => f.ruleId))];
|
|
8205
8329
|
const sarif = {
|
|
8206
8330
|
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
@@ -8208,14 +8332,24 @@ function formatSarif(report, rawText) {
|
|
|
8208
8332
|
runs: [{
|
|
8209
8333
|
tool: { driver: { name: "glotfile", rules: ruleIds.map((id) => ({ id })) } },
|
|
8210
8334
|
results: report.findings.map((f) => {
|
|
8211
|
-
const
|
|
8212
|
-
return {
|
|
8335
|
+
const base = {
|
|
8213
8336
|
ruleId: f.ruleId,
|
|
8214
|
-
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,
|
|
8215
8349
|
message: { text: `${f.key}${f.locale ? ` [${f.locale}]` : ""}: ${f.message}` },
|
|
8216
8350
|
locations: [{
|
|
8217
8351
|
physicalLocation: {
|
|
8218
|
-
artifactLocation: { uri:
|
|
8352
|
+
artifactLocation: { uri: ctx.keysUri },
|
|
8219
8353
|
region: { startLine: pos.line, startColumn: pos.column }
|
|
8220
8354
|
}
|
|
8221
8355
|
}]
|
|
@@ -8630,9 +8764,23 @@ async function runContextBatchAction(args, pending, action, projectRoot) {
|
|
|
8630
8764
|
if (outcome.retried) console.log(`${outcome.retried} job(s) re-run synchronously (batch entries failed or were malformed).`);
|
|
8631
8765
|
for (const e of outcome.errors) console.warn(`skip ${e.key}: ${e.error}`);
|
|
8632
8766
|
}
|
|
8633
|
-
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) {
|
|
8634
8782
|
if (format === "json") console.log(formatJson(report).trimEnd());
|
|
8635
|
-
else if (format === "sarif") console.log(formatSarif(report,
|
|
8783
|
+
else if (format === "sarif") console.log(formatSarif(report, sarifContextFor(statePath)).trimEnd());
|
|
8636
8784
|
else console.log(formatText(report).trimEnd());
|
|
8637
8785
|
}
|
|
8638
8786
|
async function runLintCmd(args) {
|
|
@@ -8649,13 +8797,12 @@ async function runLintCmd(args) {
|
|
|
8649
8797
|
}
|
|
8650
8798
|
return;
|
|
8651
8799
|
}
|
|
8652
|
-
const rawText = existsSync14(args.statePath) ? readFileSync24(args.statePath, "utf8") : "";
|
|
8653
8800
|
const report = await runLint(state, {
|
|
8654
8801
|
locales: args.locales,
|
|
8655
8802
|
ruleIds: args.ruleIds,
|
|
8656
8803
|
includeSuppressed: args.includeSuppressed
|
|
8657
8804
|
});
|
|
8658
|
-
printReport(report, args.format,
|
|
8805
|
+
printReport(report, args.format, args.statePath);
|
|
8659
8806
|
const tooManyWarnings = args.maxWarnings != null && report.counts.warn > args.maxWarnings;
|
|
8660
8807
|
if (!report.ok || tooManyWarnings) process.exitCode = 1;
|
|
8661
8808
|
}
|
|
@@ -8669,17 +8816,16 @@ async function runCheck(args) {
|
|
|
8669
8816
|
counts: { error: 1, warn: 0, suppressed: 0 },
|
|
8670
8817
|
ok: false
|
|
8671
8818
|
};
|
|
8672
|
-
printReport(report2, args.format,
|
|
8819
|
+
printReport(report2, args.format, args.statePath);
|
|
8673
8820
|
process.exitCode = 1;
|
|
8674
8821
|
return;
|
|
8675
8822
|
}
|
|
8676
|
-
const rawText = existsSync14(args.statePath) ? readFileSync24(args.statePath, "utf8") : "";
|
|
8677
8823
|
const root = dirname5(resolve11(args.statePath));
|
|
8678
8824
|
const lint = await runLint(state, {});
|
|
8679
8825
|
const findings = sortFindings([...lint.findings, ...checkOutputs(state, root)]);
|
|
8680
8826
|
const counts = { ...countSeverities(findings), suppressed: lint.counts.suppressed };
|
|
8681
8827
|
const report = { findings, counts, ok: counts.error === 0 };
|
|
8682
|
-
printReport(report, args.format,
|
|
8828
|
+
printReport(report, args.format, args.statePath);
|
|
8683
8829
|
if (!report.ok) process.exitCode = 1;
|
|
8684
8830
|
}
|
|
8685
8831
|
async function runImportCmd(args) {
|
|
@@ -8880,6 +9026,7 @@ async function runPrune(args) {
|
|
|
8880
9026
|
}
|
|
8881
9027
|
const state = loadState(args.statePath);
|
|
8882
9028
|
const toRemove = /* @__PURE__ */ new Set();
|
|
9029
|
+
let heuristicUnused = false;
|
|
8883
9030
|
if (args.emptySource) {
|
|
8884
9031
|
for (const k of findEmptySourceKeys(state)) toRemove.add(k);
|
|
8885
9032
|
}
|
|
@@ -8895,6 +9042,7 @@ async function runPrune(args) {
|
|
|
8895
9042
|
for (const k of Object.keys(state.keys)) {
|
|
8896
9043
|
if (!used.has(k)) toRemove.add(k);
|
|
8897
9044
|
}
|
|
9045
|
+
heuristicUnused = true;
|
|
8898
9046
|
}
|
|
8899
9047
|
}
|
|
8900
9048
|
const keys = [...toRemove].sort();
|
|
@@ -8902,6 +9050,11 @@ async function runPrune(args) {
|
|
|
8902
9050
|
console.log("No keys to prune.");
|
|
8903
9051
|
return;
|
|
8904
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
|
+
}
|
|
8905
9058
|
if (!args.write) {
|
|
8906
9059
|
for (const k of keys) console.log(k);
|
|
8907
9060
|
console.log(`${keys.length} key(s) to prune. Run with --write to remove them.`);
|