glotfile 0.7.5 → 0.8.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 +1517 -1216
- package/dist/server/server.js +210 -9
- package/dist/ui/assets/index-Cgnutw-J.css +1 -0
- package/dist/ui/assets/index-DtQaiBsM.js +2299 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/skill/SKILL.md +4 -2
- package/skill/references/cli-reference.md +21 -1
- package/skill/references/workflows.md +25 -5
- package/dist/ui/assets/index-BvrhsGHu.js +0 -2124
- package/dist/ui/assets/index-dSBo_QMR.css +0 -1
package/dist/server/cli.js
CHANGED
|
@@ -4186,592 +4186,168 @@ var init_scanner = __esm({
|
|
|
4186
4186
|
}
|
|
4187
4187
|
});
|
|
4188
4188
|
|
|
4189
|
-
// src/server/
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
};
|
|
4198
|
-
for (const e of glossary) {
|
|
4199
|
-
add(e.term);
|
|
4200
|
-
for (const t of Object.values(e.translations ?? {})) add(t);
|
|
4189
|
+
// src/server/import/detect.ts
|
|
4190
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
4191
|
+
import { join as join6 } from "path";
|
|
4192
|
+
function safeIsDir(p) {
|
|
4193
|
+
try {
|
|
4194
|
+
return statSync3(p).isDirectory();
|
|
4195
|
+
} catch {
|
|
4196
|
+
return false;
|
|
4201
4197
|
}
|
|
4202
|
-
for (const w of customWords) add(w);
|
|
4203
|
-
return set;
|
|
4204
4198
|
}
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
if (unavailable.has(key)) return null;
|
|
4199
|
+
function listDirs(dir) {
|
|
4200
|
+
return readdirSync4(dir).filter((e) => safeIsDir(join6(dir, e)));
|
|
4201
|
+
}
|
|
4202
|
+
function fileCount(dir) {
|
|
4210
4203
|
try {
|
|
4211
|
-
|
|
4212
|
-
const nspell = nspellMod.default ?? nspellMod;
|
|
4213
|
-
const dictMod = await import(`dictionary-${key}`);
|
|
4214
|
-
const dictExport = dictMod.default ?? dictMod;
|
|
4215
|
-
const dict = typeof dictExport === "function" ? await dictExport() : dictExport;
|
|
4216
|
-
const speller = nspell(dict);
|
|
4217
|
-
instances.set(key, speller);
|
|
4218
|
-
return speller;
|
|
4204
|
+
return readdirSync4(dir).length;
|
|
4219
4205
|
} catch {
|
|
4220
|
-
|
|
4221
|
-
return null;
|
|
4222
|
-
} finally {
|
|
4223
|
-
loading.delete(key);
|
|
4206
|
+
return 0;
|
|
4224
4207
|
}
|
|
4225
4208
|
}
|
|
4226
|
-
function
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4209
|
+
function pickSource(locales, sizeOf) {
|
|
4210
|
+
if (locales.includes("en")) return "en";
|
|
4211
|
+
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
4212
|
+
}
|
|
4213
|
+
function detectLaravel(root) {
|
|
4214
|
+
const localeRoot = [join6(root, "resources", "lang"), join6(root, "lang")].find(safeIsDir);
|
|
4215
|
+
if (!localeRoot) return null;
|
|
4216
|
+
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
4217
|
+
if (locales.length === 0) return null;
|
|
4218
|
+
const sourceLocale = pickSource(locales, (loc) => fileCount(join6(localeRoot, loc)));
|
|
4219
|
+
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
4220
|
+
}
|
|
4221
|
+
function detectVue(root, forced = false) {
|
|
4222
|
+
for (const rel of VUE_DIR_CANDIDATES) {
|
|
4223
|
+
const localeRoot = join6(root, rel);
|
|
4224
|
+
if (!safeIsDir(localeRoot)) continue;
|
|
4225
|
+
const locales = readdirSync4(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4226
|
+
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
4227
|
+
if (enough) {
|
|
4228
|
+
const sourceLocale = pickSource(locales, (loc) => {
|
|
4229
|
+
try {
|
|
4230
|
+
return statSync3(join6(localeRoot, `${loc}.json`)).size;
|
|
4231
|
+
} catch {
|
|
4232
|
+
return 0;
|
|
4233
|
+
}
|
|
4234
|
+
});
|
|
4235
|
+
return { format: "vue-i18n-json", localeRoot, locales, sourceLocale };
|
|
4234
4236
|
}
|
|
4235
|
-
return null;
|
|
4236
4237
|
}
|
|
4237
|
-
|
|
4238
|
-
let allBad = cache.get(cacheKey);
|
|
4239
|
-
if (!allBad) {
|
|
4240
|
-
allBad = spellTokens(value).filter((w) => !spell.correct(w));
|
|
4241
|
-
cache.set(cacheKey, allBad);
|
|
4242
|
-
}
|
|
4243
|
-
return allBad.filter((w) => !ignore.has(w.toLowerCase()));
|
|
4238
|
+
return null;
|
|
4244
4239
|
}
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
norm = (dictId) => dictId.toLowerCase();
|
|
4254
|
-
ICU_BLOCK = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g;
|
|
4255
|
-
MASK = /\{[^}]*\}|<[^>]*>|:\w+|%[sd]/g;
|
|
4256
|
-
WORD = new RegExp("\\p{L}[\\p{L}''\u2019-]*", "gu");
|
|
4240
|
+
function detectArb(root) {
|
|
4241
|
+
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
4242
|
+
const localeRoot = join6(root, rel);
|
|
4243
|
+
if (!safeIsDir(localeRoot)) continue;
|
|
4244
|
+
const locales = readdirSync4(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
4245
|
+
if (locales.length >= 1) {
|
|
4246
|
+
return { format: "flutter-arb", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
4247
|
+
}
|
|
4257
4248
|
}
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
if (!speller.correct(word)) {
|
|
4280
|
-
out.push({ ruleId: "spelling", key, locale, message: `possible misspelling: "${word}"` });
|
|
4281
|
-
}
|
|
4282
|
-
}
|
|
4249
|
+
return null;
|
|
4250
|
+
}
|
|
4251
|
+
function lprojLocales(dir) {
|
|
4252
|
+
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join6(dir, `${l}.lproj`, "Localizable.strings")));
|
|
4253
|
+
}
|
|
4254
|
+
function detectApple(root) {
|
|
4255
|
+
const candidates = [root, ...listDirs(root).map((d) => join6(root, d))];
|
|
4256
|
+
let best = null;
|
|
4257
|
+
for (const dir of candidates) {
|
|
4258
|
+
const locales = lprojLocales(dir);
|
|
4259
|
+
if (locales.length === 0) continue;
|
|
4260
|
+
if (!best || locales.length > best.locales.length) {
|
|
4261
|
+
best = {
|
|
4262
|
+
format: "apple-strings",
|
|
4263
|
+
localeRoot: dir,
|
|
4264
|
+
locales,
|
|
4265
|
+
sourceLocale: pickSource(locales, (loc) => {
|
|
4266
|
+
try {
|
|
4267
|
+
return statSync3(join6(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
4268
|
+
} catch {
|
|
4269
|
+
return 0;
|
|
4283
4270
|
}
|
|
4284
|
-
}
|
|
4285
|
-
|
|
4271
|
+
})
|
|
4272
|
+
};
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
return best;
|
|
4276
|
+
}
|
|
4277
|
+
function detectAngularXliff(root) {
|
|
4278
|
+
for (const rel of ANGULAR_DIR_CANDIDATES) {
|
|
4279
|
+
const localeRoot = rel === "." ? root : join6(root, rel);
|
|
4280
|
+
if (!safeIsDir(localeRoot)) continue;
|
|
4281
|
+
const files = readdirSync4(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
|
|
4282
|
+
if (files.length === 0) continue;
|
|
4283
|
+
const locales = files.map((f) => f.match(/^messages\.(.+)\.xlf$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
4284
|
+
const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
|
|
4285
|
+
let sourceLocale;
|
|
4286
|
+
try {
|
|
4287
|
+
sourceLocale = readFileSync12(join6(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
|
|
4288
|
+
} catch {
|
|
4289
|
+
}
|
|
4290
|
+
if (!sourceLocale && locales.length === 0) continue;
|
|
4291
|
+
sourceLocale ??= pickSource(locales, () => 0);
|
|
4292
|
+
if (!locales.includes(sourceLocale)) locales.unshift(sourceLocale);
|
|
4293
|
+
return { format: "angular-xliff", localeRoot, locales, sourceLocale };
|
|
4294
|
+
}
|
|
4295
|
+
return null;
|
|
4296
|
+
}
|
|
4297
|
+
function detectRails(root) {
|
|
4298
|
+
const localeRoot = join6(root, "config", "locales");
|
|
4299
|
+
if (!safeIsDir(localeRoot)) return null;
|
|
4300
|
+
const locales = [];
|
|
4301
|
+
for (const file of readdirSync4(localeRoot).sort()) {
|
|
4302
|
+
if (!/\.ya?ml$/.test(file)) continue;
|
|
4303
|
+
let text;
|
|
4304
|
+
try {
|
|
4305
|
+
text = readFileSync12(join6(localeRoot, file), "utf8");
|
|
4306
|
+
} catch {
|
|
4307
|
+
continue;
|
|
4308
|
+
}
|
|
4309
|
+
for (const m of text.matchAll(/^(["']?)([A-Za-z][\w-]*)\1:\s*(?:#.*)?$/gm)) {
|
|
4310
|
+
const token = m[2];
|
|
4311
|
+
if (LOCALE_RE.test(token) && !locales.includes(token)) locales.push(token);
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
if (locales.length === 0) return null;
|
|
4315
|
+
return { format: "rails-yaml", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
4316
|
+
}
|
|
4317
|
+
function detectI18next(root) {
|
|
4318
|
+
for (const rel of I18NEXT_DIR_CANDIDATES) {
|
|
4319
|
+
const localeRoot = join6(root, rel);
|
|
4320
|
+
if (!safeIsDir(localeRoot)) continue;
|
|
4321
|
+
const locales = listDirs(localeRoot).filter(
|
|
4322
|
+
(d) => LOCALE_RE.test(d) && readdirSync4(join6(localeRoot, d)).some((f) => f.endsWith(".json"))
|
|
4323
|
+
);
|
|
4324
|
+
if (locales.length === 0) continue;
|
|
4325
|
+
const sourceLocale = pickSource(locales, (loc) => {
|
|
4326
|
+
try {
|
|
4327
|
+
return readdirSync4(join6(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync3(join6(localeRoot, loc, f)).size, 0);
|
|
4328
|
+
} catch {
|
|
4329
|
+
return 0;
|
|
4286
4330
|
}
|
|
4287
|
-
};
|
|
4288
|
-
|
|
4331
|
+
});
|
|
4332
|
+
return { format: "i18next-json", localeRoot, locales, sourceLocale };
|
|
4289
4333
|
}
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
const v = entry.plural ? entry.values[ctx.sourceLocale]?.forms?.other : entry.values[ctx.sourceLocale]?.value;
|
|
4308
|
-
if (!v || !v.trim()) out.push({ ruleId: "empty-source", key, locale: "", message: "source value is empty" });
|
|
4309
|
-
}
|
|
4310
|
-
return out;
|
|
4311
|
-
}
|
|
4312
|
-
};
|
|
4313
|
-
emptyTranslationRule = {
|
|
4314
|
-
id: "empty-translation",
|
|
4315
|
-
// findMissing is the shared "untranslated" walk (also behind the editor's
|
|
4316
|
-
// untranslated check and /scan/missing); a whitespace-only value counts as
|
|
4317
|
-
// missing there, so no separate whitespace pass is needed.
|
|
4318
|
-
run(state) {
|
|
4319
|
-
const out = [];
|
|
4320
|
-
for (const m of findMissing(state)) {
|
|
4321
|
-
out.push({ ruleId: "empty-translation", key: m.key, locale: m.locale, message: "translation is empty or missing" });
|
|
4322
|
-
}
|
|
4323
|
-
return out;
|
|
4324
|
-
}
|
|
4325
|
-
};
|
|
4326
|
-
identicalToSourceRule = {
|
|
4327
|
-
id: "identical-to-source",
|
|
4328
|
-
run(state, ctx) {
|
|
4329
|
-
const out = [];
|
|
4330
|
-
for (const key of Object.keys(state.keys)) {
|
|
4331
|
-
const entry = state.keys[key];
|
|
4332
|
-
if (entry.skipTranslate) continue;
|
|
4333
|
-
const src = entry.values[ctx.sourceLocale]?.value;
|
|
4334
|
-
if (!src) continue;
|
|
4335
|
-
for (const locale of ctx.targetLocales) {
|
|
4336
|
-
const v = entry.values[locale]?.value;
|
|
4337
|
-
if (v && v === src) out.push({ ruleId: "identical-to-source", key, locale, message: "translation is identical to the source" });
|
|
4338
|
-
}
|
|
4339
|
-
}
|
|
4340
|
-
return out;
|
|
4341
|
-
}
|
|
4342
|
-
};
|
|
4343
|
-
whitespaceRule = {
|
|
4344
|
-
id: "whitespace",
|
|
4345
|
-
run(state, ctx) {
|
|
4346
|
-
const out = [];
|
|
4347
|
-
for (const key of Object.keys(state.keys)) {
|
|
4348
|
-
const entry = state.keys[key];
|
|
4349
|
-
const src = entry.values[ctx.sourceLocale]?.value ?? "";
|
|
4350
|
-
const srcEdge = src !== src.trim();
|
|
4351
|
-
for (const locale of ctx.targetLocales) {
|
|
4352
|
-
const v = entry.values[locale]?.value;
|
|
4353
|
-
if (!v) continue;
|
|
4354
|
-
if (v !== v.trim() !== srcEdge) {
|
|
4355
|
-
out.push({ ruleId: "whitespace", key, locale, message: "leading/trailing whitespace differs from the source" });
|
|
4356
|
-
}
|
|
4357
|
-
}
|
|
4358
|
-
}
|
|
4359
|
-
return out;
|
|
4360
|
-
}
|
|
4361
|
-
};
|
|
4362
|
-
placeholderMismatchRule = {
|
|
4363
|
-
id: "placeholder-mismatch",
|
|
4364
|
-
run(state, ctx) {
|
|
4365
|
-
const out = [];
|
|
4366
|
-
for (const key of Object.keys(state.keys)) {
|
|
4367
|
-
const entry = state.keys[key];
|
|
4368
|
-
if (entry.plural) {
|
|
4369
|
-
const srcForm = entry.values[ctx.sourceLocale]?.forms?.other;
|
|
4370
|
-
if (!srcForm) continue;
|
|
4371
|
-
for (const locale of ctx.targetLocales) {
|
|
4372
|
-
const forms = entry.values[locale]?.forms;
|
|
4373
|
-
if (!forms) continue;
|
|
4374
|
-
const bad = Object.entries(forms).some(
|
|
4375
|
-
([cat, form]) => form && !pluralFormPlaceholdersMatch(cat, srcForm, form)
|
|
4376
|
-
);
|
|
4377
|
-
if (bad) {
|
|
4378
|
-
out.push({ ruleId: "placeholder-mismatch", key, locale, message: "placeholders differ from the source" });
|
|
4379
|
-
}
|
|
4380
|
-
}
|
|
4381
|
-
continue;
|
|
4382
|
-
}
|
|
4383
|
-
const src = entry.values[ctx.sourceLocale]?.value;
|
|
4384
|
-
if (!src) continue;
|
|
4385
|
-
for (const locale of ctx.targetLocales) {
|
|
4386
|
-
const v = entry.values[locale]?.value;
|
|
4387
|
-
if (!v) continue;
|
|
4388
|
-
if (!placeholdersMatch(src, v)) {
|
|
4389
|
-
out.push({ ruleId: "placeholder-mismatch", key, locale, message: "placeholders differ from the source" });
|
|
4390
|
-
}
|
|
4391
|
-
}
|
|
4392
|
-
}
|
|
4393
|
-
return out;
|
|
4394
|
-
}
|
|
4395
|
-
};
|
|
4396
|
-
icuMismatchRule = {
|
|
4397
|
-
id: "icu-mismatch",
|
|
4398
|
-
run(state, ctx) {
|
|
4399
|
-
const out = [];
|
|
4400
|
-
for (const key of Object.keys(state.keys)) {
|
|
4401
|
-
const entry = state.keys[key];
|
|
4402
|
-
const src = entry.values[ctx.sourceLocale]?.value;
|
|
4403
|
-
if (!src) continue;
|
|
4404
|
-
const srcIcu = isIcuPluralOrSelect(src);
|
|
4405
|
-
for (const locale of ctx.targetLocales) {
|
|
4406
|
-
const v = entry.values[locale]?.value;
|
|
4407
|
-
if (!v) continue;
|
|
4408
|
-
if (isIcuPluralOrSelect(v) !== srcIcu) {
|
|
4409
|
-
out.push({
|
|
4410
|
-
ruleId: "icu-mismatch",
|
|
4411
|
-
key,
|
|
4412
|
-
locale,
|
|
4413
|
-
message: srcIcu ? "source is an ICU plural/select but the translation is not" : "translation is an ICU plural/select but the source is not"
|
|
4414
|
-
});
|
|
4415
|
-
}
|
|
4416
|
-
}
|
|
4417
|
-
}
|
|
4418
|
-
return out;
|
|
4419
|
-
}
|
|
4420
|
-
};
|
|
4421
|
-
maxLengthRule = {
|
|
4422
|
-
id: "max-length",
|
|
4423
|
-
run(state, ctx) {
|
|
4424
|
-
const out = [];
|
|
4425
|
-
for (const key of Object.keys(state.keys)) {
|
|
4426
|
-
const entry = state.keys[key];
|
|
4427
|
-
const max = entry.maxLength;
|
|
4428
|
-
if (max == null) continue;
|
|
4429
|
-
for (const locale of ctx.targetLocales) {
|
|
4430
|
-
const v = entry.values[locale]?.value;
|
|
4431
|
-
if (v && v.length > max) {
|
|
4432
|
-
out.push({ ruleId: "max-length", key, locale, message: `length ${v.length} exceeds maxLength ${max}` });
|
|
4433
|
-
}
|
|
4434
|
-
}
|
|
4435
|
-
}
|
|
4436
|
-
return out;
|
|
4437
|
-
}
|
|
4438
|
-
};
|
|
4439
|
-
glossaryViolationRule = {
|
|
4440
|
-
id: "glossary-violation",
|
|
4441
|
-
run(state, ctx) {
|
|
4442
|
-
const out = [];
|
|
4443
|
-
for (const key of Object.keys(state.keys)) {
|
|
4444
|
-
const entry = state.keys[key];
|
|
4445
|
-
const src = entry.values[ctx.sourceLocale]?.value;
|
|
4446
|
-
if (!src) continue;
|
|
4447
|
-
for (const locale of ctx.targetLocales) {
|
|
4448
|
-
const v = entry.values[locale]?.value;
|
|
4449
|
-
if (!v) continue;
|
|
4450
|
-
for (const viol of glossaryViolations(src, v, locale, ctx.glossary)) {
|
|
4451
|
-
out.push({
|
|
4452
|
-
ruleId: "glossary-violation",
|
|
4453
|
-
key,
|
|
4454
|
-
locale,
|
|
4455
|
-
message: viol.kind === "do-not-translate" ? `do-not-translate term "${viol.term}" is missing or altered` : `expected glossary translation "${viol.expected}" for "${viol.term}"`
|
|
4456
|
-
});
|
|
4457
|
-
}
|
|
4458
|
-
}
|
|
4459
|
-
}
|
|
4460
|
-
return out;
|
|
4461
|
-
}
|
|
4462
|
-
};
|
|
4463
|
-
ALL_RULES = [
|
|
4464
|
-
emptySourceRule,
|
|
4465
|
-
emptyTranslationRule,
|
|
4466
|
-
placeholderMismatchRule,
|
|
4467
|
-
icuMismatchRule,
|
|
4468
|
-
glossaryViolationRule,
|
|
4469
|
-
maxLengthRule,
|
|
4470
|
-
identicalToSourceRule,
|
|
4471
|
-
whitespaceRule,
|
|
4472
|
-
spellingRule
|
|
4473
|
-
];
|
|
4474
|
-
}
|
|
4475
|
-
});
|
|
4476
|
-
|
|
4477
|
-
// src/server/lint/run.ts
|
|
4478
|
-
function resolveSeverity(id, config) {
|
|
4479
|
-
return config.rules?.[id] ?? DEFAULT_SEVERITY[id];
|
|
4480
|
-
}
|
|
4481
|
-
function sortFindings(findings) {
|
|
4482
|
-
return [...findings].sort(
|
|
4483
|
-
(a, b) => a.key.localeCompare(b.key) || a.locale.localeCompare(b.locale) || a.ruleId.localeCompare(b.ruleId)
|
|
4484
|
-
);
|
|
4485
|
-
}
|
|
4486
|
-
function countSeverities(findings) {
|
|
4487
|
-
let error = 0, warn = 0;
|
|
4488
|
-
for (const f of findings) {
|
|
4489
|
-
if (f.suppressed) continue;
|
|
4490
|
-
f.severity === "error" ? error++ : warn++;
|
|
4491
|
-
}
|
|
4492
|
-
return { error, warn };
|
|
4493
|
-
}
|
|
4494
|
-
async function loadSpellers(locales, config, load, warn) {
|
|
4495
|
-
const map = /* @__PURE__ */ new Map();
|
|
4496
|
-
for (const locale of locales) {
|
|
4497
|
-
const dictId = config.spelling?.locales?.[locale] ?? locale;
|
|
4498
|
-
const speller = await load(dictId);
|
|
4499
|
-
if (speller) map.set(locale, speller);
|
|
4500
|
-
else warn(`no dictionary for "${locale}", skipping spelling`);
|
|
4501
|
-
}
|
|
4502
|
-
return map;
|
|
4503
|
-
}
|
|
4504
|
-
async function runLint(state, options = {}) {
|
|
4505
|
-
const config = state.config.lint ?? {};
|
|
4506
|
-
const rules = options.rules ?? ALL_RULES;
|
|
4507
|
-
const warn = options.warn ?? ((m) => console.warn(m));
|
|
4508
|
-
const load = options.loadSpeller ?? defaultLoader;
|
|
4509
|
-
const targetLocales = state.config.locales.filter((l) => l !== state.config.sourceLocale);
|
|
4510
|
-
const isActive = (rule) => {
|
|
4511
|
-
if (options.ruleIds && !options.ruleIds.includes(rule.id)) return false;
|
|
4512
|
-
return resolveSeverity(rule.id, config) !== "off";
|
|
4513
|
-
};
|
|
4514
|
-
const active = rules.filter(isActive);
|
|
4515
|
-
const spellingOn = active.some((r) => r.id === "spelling");
|
|
4516
|
-
const spellers = spellingOn ? await loadSpellers(targetLocales, config, load, warn) : /* @__PURE__ */ new Map();
|
|
4517
|
-
const allowWords = spellingOn ? ignoreWordsFor(state.glossary, state.config.spelling?.customWords) : /* @__PURE__ */ new Set();
|
|
4518
|
-
const ctx = {
|
|
4519
|
-
config,
|
|
4520
|
-
sourceLocale: state.config.sourceLocale,
|
|
4521
|
-
targetLocales,
|
|
4522
|
-
glossary: state.glossary,
|
|
4523
|
-
spellers,
|
|
4524
|
-
allowWords
|
|
4525
|
-
};
|
|
4526
|
-
const ignoreRes = (config.ignore ?? []).map(globToRegExp);
|
|
4527
|
-
const localeFilter = options.locales ? new Set(options.locales) : null;
|
|
4528
|
-
const findings = [];
|
|
4529
|
-
let suppressed = 0;
|
|
4530
|
-
for (const rule of active) {
|
|
4531
|
-
const severity = resolveSeverity(rule.id, config);
|
|
4532
|
-
for (const raw of rule.run(state, ctx)) {
|
|
4533
|
-
if (ignoreRes.some((re) => re.test(raw.key))) continue;
|
|
4534
|
-
if (localeFilter && raw.locale !== "" && !localeFilter.has(raw.locale)) continue;
|
|
4535
|
-
const entry = state.keys[raw.key];
|
|
4536
|
-
if (raw.locale !== "" && entry && findSuppression(entry, state.config.sourceLocale, rule.id, raw.locale)) {
|
|
4537
|
-
suppressed++;
|
|
4538
|
-
if (options.includeSuppressed) findings.push({ ...raw, severity, suppressed: true });
|
|
4539
|
-
continue;
|
|
4540
|
-
}
|
|
4541
|
-
findings.push({ ...raw, severity });
|
|
4542
|
-
}
|
|
4543
|
-
}
|
|
4544
|
-
const sorted = sortFindings(findings);
|
|
4545
|
-
const counts = { ...countSeverities(sorted), suppressed };
|
|
4546
|
-
return { findings: sorted, counts, ok: counts.error === 0 };
|
|
4547
|
-
}
|
|
4548
|
-
var init_run2 = __esm({
|
|
4549
|
-
"src/server/lint/run.ts"() {
|
|
4550
|
-
"use strict";
|
|
4551
|
-
init_glob();
|
|
4552
|
-
init_registry();
|
|
4553
|
-
init_rules();
|
|
4554
|
-
init_spelling();
|
|
4555
|
-
init_spell();
|
|
4556
|
-
init_suppress();
|
|
4557
|
-
}
|
|
4558
|
-
});
|
|
4559
|
-
|
|
4560
|
-
// src/server/lint/outputs.ts
|
|
4561
|
-
import { readFileSync as readFileSync12, existsSync as existsSync11 } from "fs";
|
|
4562
|
-
import { resolve as resolve8 } from "path";
|
|
4563
|
-
function checkOutputs(state, root) {
|
|
4564
|
-
const out = [];
|
|
4565
|
-
for (const output of state.config.outputs) {
|
|
4566
|
-
const result = getAdapter(output.adapter).export(state, output);
|
|
4567
|
-
for (const file of result.files) {
|
|
4568
|
-
const abs = resolve8(root, file.path);
|
|
4569
|
-
const current = existsSync11(abs) ? readFileSync12(abs, "utf8") : null;
|
|
4570
|
-
if (current === null) {
|
|
4571
|
-
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
4572
|
-
} else if (current !== file.contents) {
|
|
4573
|
-
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is out of date; run `glotfile export`" });
|
|
4574
|
-
}
|
|
4575
|
-
}
|
|
4576
|
-
}
|
|
4577
|
-
return out;
|
|
4578
|
-
}
|
|
4579
|
-
var init_outputs = __esm({
|
|
4580
|
-
"src/server/lint/outputs.ts"() {
|
|
4581
|
-
"use strict";
|
|
4582
|
-
init_adapters();
|
|
4583
|
-
}
|
|
4584
|
-
});
|
|
4585
|
-
|
|
4586
|
-
// src/server/lint/accept.ts
|
|
4587
|
-
var accept_exports = {};
|
|
4588
|
-
__export(accept_exports, {
|
|
4589
|
-
acceptFindings: () => acceptFindings
|
|
4590
|
-
});
|
|
4591
|
-
function acceptFindings(state, findings, opts = {}, clock = systemClock) {
|
|
4592
|
-
const byRule = {};
|
|
4593
|
-
let accepted = 0;
|
|
4594
|
-
for (const f of findings) {
|
|
4595
|
-
if (f.locale === "" || f.suppressed) continue;
|
|
4596
|
-
if (f.severity === "error" && !opts.includeErrors) continue;
|
|
4597
|
-
if (opts.rules && !opts.rules.includes(f.ruleId)) continue;
|
|
4598
|
-
if (opts.locales && !opts.locales.includes(f.locale)) continue;
|
|
4599
|
-
if (!state.keys[f.key]) continue;
|
|
4600
|
-
addSuppression(state, f.key, f.ruleId, f.locale, clock);
|
|
4601
|
-
byRule[f.ruleId] = (byRule[f.ruleId] ?? 0) + 1;
|
|
4602
|
-
accepted++;
|
|
4603
|
-
}
|
|
4604
|
-
return { accepted, byRule };
|
|
4605
|
-
}
|
|
4606
|
-
var init_accept = __esm({
|
|
4607
|
-
"src/server/lint/accept.ts"() {
|
|
4608
|
-
"use strict";
|
|
4609
|
-
init_state();
|
|
4610
|
-
}
|
|
4611
|
-
});
|
|
4612
|
-
|
|
4613
|
-
// src/server/import/detect.ts
|
|
4614
|
-
import { existsSync as existsSync12, readdirSync as readdirSync4, readFileSync as readFileSync13, statSync as statSync3 } from "fs";
|
|
4615
|
-
import { join as join6 } from "path";
|
|
4616
|
-
function safeIsDir(p) {
|
|
4617
|
-
try {
|
|
4618
|
-
return statSync3(p).isDirectory();
|
|
4619
|
-
} catch {
|
|
4620
|
-
return false;
|
|
4621
|
-
}
|
|
4622
|
-
}
|
|
4623
|
-
function listDirs(dir) {
|
|
4624
|
-
return readdirSync4(dir).filter((e) => safeIsDir(join6(dir, e)));
|
|
4625
|
-
}
|
|
4626
|
-
function fileCount(dir) {
|
|
4627
|
-
try {
|
|
4628
|
-
return readdirSync4(dir).length;
|
|
4629
|
-
} catch {
|
|
4630
|
-
return 0;
|
|
4631
|
-
}
|
|
4632
|
-
}
|
|
4633
|
-
function pickSource(locales, sizeOf) {
|
|
4634
|
-
if (locales.includes("en")) return "en";
|
|
4635
|
-
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
4636
|
-
}
|
|
4637
|
-
function detectLaravel(root) {
|
|
4638
|
-
const localeRoot = [join6(root, "resources", "lang"), join6(root, "lang")].find(safeIsDir);
|
|
4639
|
-
if (!localeRoot) return null;
|
|
4640
|
-
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
4641
|
-
if (locales.length === 0) return null;
|
|
4642
|
-
const sourceLocale = pickSource(locales, (loc) => fileCount(join6(localeRoot, loc)));
|
|
4643
|
-
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
4644
|
-
}
|
|
4645
|
-
function detectVue(root, forced = false) {
|
|
4646
|
-
for (const rel of VUE_DIR_CANDIDATES) {
|
|
4647
|
-
const localeRoot = join6(root, rel);
|
|
4648
|
-
if (!safeIsDir(localeRoot)) continue;
|
|
4649
|
-
const locales = readdirSync4(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4650
|
-
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
4651
|
-
if (enough) {
|
|
4652
|
-
const sourceLocale = pickSource(locales, (loc) => {
|
|
4653
|
-
try {
|
|
4654
|
-
return statSync3(join6(localeRoot, `${loc}.json`)).size;
|
|
4655
|
-
} catch {
|
|
4656
|
-
return 0;
|
|
4657
|
-
}
|
|
4658
|
-
});
|
|
4659
|
-
return { format: "vue-i18n-json", localeRoot, locales, sourceLocale };
|
|
4660
|
-
}
|
|
4661
|
-
}
|
|
4662
|
-
return null;
|
|
4663
|
-
}
|
|
4664
|
-
function detectArb(root) {
|
|
4665
|
-
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
4666
|
-
const localeRoot = join6(root, rel);
|
|
4667
|
-
if (!safeIsDir(localeRoot)) continue;
|
|
4668
|
-
const locales = readdirSync4(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
4669
|
-
if (locales.length >= 1) {
|
|
4670
|
-
return { format: "flutter-arb", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
4671
|
-
}
|
|
4672
|
-
}
|
|
4673
|
-
return null;
|
|
4674
|
-
}
|
|
4675
|
-
function lprojLocales(dir) {
|
|
4676
|
-
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync12(join6(dir, `${l}.lproj`, "Localizable.strings")));
|
|
4677
|
-
}
|
|
4678
|
-
function detectApple(root) {
|
|
4679
|
-
const candidates = [root, ...listDirs(root).map((d) => join6(root, d))];
|
|
4680
|
-
let best = null;
|
|
4681
|
-
for (const dir of candidates) {
|
|
4682
|
-
const locales = lprojLocales(dir);
|
|
4683
|
-
if (locales.length === 0) continue;
|
|
4684
|
-
if (!best || locales.length > best.locales.length) {
|
|
4685
|
-
best = {
|
|
4686
|
-
format: "apple-strings",
|
|
4687
|
-
localeRoot: dir,
|
|
4688
|
-
locales,
|
|
4689
|
-
sourceLocale: pickSource(locales, (loc) => {
|
|
4690
|
-
try {
|
|
4691
|
-
return statSync3(join6(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
4692
|
-
} catch {
|
|
4693
|
-
return 0;
|
|
4694
|
-
}
|
|
4695
|
-
})
|
|
4696
|
-
};
|
|
4697
|
-
}
|
|
4698
|
-
}
|
|
4699
|
-
return best;
|
|
4700
|
-
}
|
|
4701
|
-
function detectAngularXliff(root) {
|
|
4702
|
-
for (const rel of ANGULAR_DIR_CANDIDATES) {
|
|
4703
|
-
const localeRoot = rel === "." ? root : join6(root, rel);
|
|
4704
|
-
if (!safeIsDir(localeRoot)) continue;
|
|
4705
|
-
const files = readdirSync4(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
|
|
4706
|
-
if (files.length === 0) continue;
|
|
4707
|
-
const locales = files.map((f) => f.match(/^messages\.(.+)\.xlf$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
4708
|
-
const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
|
|
4709
|
-
let sourceLocale;
|
|
4710
|
-
try {
|
|
4711
|
-
sourceLocale = readFileSync13(join6(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
|
|
4712
|
-
} catch {
|
|
4713
|
-
}
|
|
4714
|
-
if (!sourceLocale && locales.length === 0) continue;
|
|
4715
|
-
sourceLocale ??= pickSource(locales, () => 0);
|
|
4716
|
-
if (!locales.includes(sourceLocale)) locales.unshift(sourceLocale);
|
|
4717
|
-
return { format: "angular-xliff", localeRoot, locales, sourceLocale };
|
|
4718
|
-
}
|
|
4719
|
-
return null;
|
|
4720
|
-
}
|
|
4721
|
-
function detectRails(root) {
|
|
4722
|
-
const localeRoot = join6(root, "config", "locales");
|
|
4723
|
-
if (!safeIsDir(localeRoot)) return null;
|
|
4724
|
-
const locales = [];
|
|
4725
|
-
for (const file of readdirSync4(localeRoot).sort()) {
|
|
4726
|
-
if (!/\.ya?ml$/.test(file)) continue;
|
|
4727
|
-
let text;
|
|
4728
|
-
try {
|
|
4729
|
-
text = readFileSync13(join6(localeRoot, file), "utf8");
|
|
4730
|
-
} catch {
|
|
4731
|
-
continue;
|
|
4732
|
-
}
|
|
4733
|
-
for (const m of text.matchAll(/^(["']?)([A-Za-z][\w-]*)\1:\s*(?:#.*)?$/gm)) {
|
|
4734
|
-
const token = m[2];
|
|
4735
|
-
if (LOCALE_RE.test(token) && !locales.includes(token)) locales.push(token);
|
|
4736
|
-
}
|
|
4737
|
-
}
|
|
4738
|
-
if (locales.length === 0) return null;
|
|
4739
|
-
return { format: "rails-yaml", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
4740
|
-
}
|
|
4741
|
-
function detectI18next(root) {
|
|
4742
|
-
for (const rel of I18NEXT_DIR_CANDIDATES) {
|
|
4743
|
-
const localeRoot = join6(root, rel);
|
|
4744
|
-
if (!safeIsDir(localeRoot)) continue;
|
|
4745
|
-
const locales = listDirs(localeRoot).filter(
|
|
4746
|
-
(d) => LOCALE_RE.test(d) && readdirSync4(join6(localeRoot, d)).some((f) => f.endsWith(".json"))
|
|
4747
|
-
);
|
|
4748
|
-
if (locales.length === 0) continue;
|
|
4749
|
-
const sourceLocale = pickSource(locales, (loc) => {
|
|
4750
|
-
try {
|
|
4751
|
-
return readdirSync4(join6(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync3(join6(localeRoot, loc, f)).size, 0);
|
|
4752
|
-
} catch {
|
|
4753
|
-
return 0;
|
|
4754
|
-
}
|
|
4755
|
-
});
|
|
4756
|
-
return { format: "i18next-json", localeRoot, locales, sourceLocale };
|
|
4757
|
-
}
|
|
4758
|
-
return null;
|
|
4759
|
-
}
|
|
4760
|
-
function gettextLocales(dir) {
|
|
4761
|
-
const locales = [];
|
|
4762
|
-
for (const entry of readdirSync4(dir).sort()) {
|
|
4763
|
-
const flat = entry.match(/^(.+)\.po$/)?.[1];
|
|
4764
|
-
if (flat && LOCALE_RE.test(flat)) {
|
|
4765
|
-
if (!locales.includes(flat)) locales.push(flat);
|
|
4766
|
-
continue;
|
|
4767
|
-
}
|
|
4768
|
-
if (!LOCALE_RE.test(entry) || !safeIsDir(join6(dir, entry))) continue;
|
|
4769
|
-
const sub = join6(dir, entry);
|
|
4770
|
-
const hasPo = (d) => {
|
|
4771
|
-
try {
|
|
4772
|
-
return readdirSync4(d).some((f) => f.endsWith(".po"));
|
|
4773
|
-
} catch {
|
|
4774
|
-
return false;
|
|
4334
|
+
return null;
|
|
4335
|
+
}
|
|
4336
|
+
function gettextLocales(dir) {
|
|
4337
|
+
const locales = [];
|
|
4338
|
+
for (const entry of readdirSync4(dir).sort()) {
|
|
4339
|
+
const flat = entry.match(/^(.+)\.po$/)?.[1];
|
|
4340
|
+
if (flat && LOCALE_RE.test(flat)) {
|
|
4341
|
+
if (!locales.includes(flat)) locales.push(flat);
|
|
4342
|
+
continue;
|
|
4343
|
+
}
|
|
4344
|
+
if (!LOCALE_RE.test(entry) || !safeIsDir(join6(dir, entry))) continue;
|
|
4345
|
+
const sub = join6(dir, entry);
|
|
4346
|
+
const hasPo = (d) => {
|
|
4347
|
+
try {
|
|
4348
|
+
return readdirSync4(d).some((f) => f.endsWith(".po"));
|
|
4349
|
+
} catch {
|
|
4350
|
+
return false;
|
|
4775
4351
|
}
|
|
4776
4352
|
};
|
|
4777
4353
|
if (hasPo(join6(sub, "LC_MESSAGES")) || hasPo(sub)) {
|
|
@@ -4794,7 +4370,7 @@ function detectAppleStringsdict(root) {
|
|
|
4794
4370
|
const candidates = [root, ...listDirs(root).map((d) => join6(root, d))];
|
|
4795
4371
|
let best = null;
|
|
4796
4372
|
for (const dir of candidates) {
|
|
4797
|
-
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) &&
|
|
4373
|
+
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join6(dir, `${l}.lproj`, "Localizable.stringsdict")));
|
|
4798
4374
|
if (locales.length === 0) continue;
|
|
4799
4375
|
if (!best || locales.length > best.locales.length) {
|
|
4800
4376
|
best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
@@ -4803,7 +4379,7 @@ function detectAppleStringsdict(root) {
|
|
|
4803
4379
|
return best;
|
|
4804
4380
|
}
|
|
4805
4381
|
function detect(root, formatOverride) {
|
|
4806
|
-
if (!
|
|
4382
|
+
if (!existsSync11(root)) return null;
|
|
4807
4383
|
if (formatOverride) {
|
|
4808
4384
|
const fn = BY_FORMAT[formatOverride];
|
|
4809
4385
|
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
@@ -4877,7 +4453,7 @@ var init_flatten = __esm({
|
|
|
4877
4453
|
});
|
|
4878
4454
|
|
|
4879
4455
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
4880
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
4456
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync13 } from "fs";
|
|
4881
4457
|
import { join as join7 } from "path";
|
|
4882
4458
|
var LOCALE_RE2, vueI18nJson2;
|
|
4883
4459
|
var init_vue_i18n_json2 = __esm({
|
|
@@ -4898,7 +4474,7 @@ var init_vue_i18n_json2 = __esm({
|
|
|
4898
4474
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4899
4475
|
let data;
|
|
4900
4476
|
try {
|
|
4901
|
-
data = JSON.parse(
|
|
4477
|
+
data = JSON.parse(readFileSync13(join7(localeRoot, file), "utf8"));
|
|
4902
4478
|
} catch (e) {
|
|
4903
4479
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
4904
4480
|
continue;
|
|
@@ -5003,7 +4579,7 @@ var init_laravel_php2 = __esm({
|
|
|
5003
4579
|
});
|
|
5004
4580
|
|
|
5005
4581
|
// src/server/import/parsers/flutter-arb.ts
|
|
5006
|
-
import { readdirSync as readdirSync7, readFileSync as
|
|
4582
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync14 } from "fs";
|
|
5007
4583
|
import { join as join9 } from "path";
|
|
5008
4584
|
function localeFromArbName(file) {
|
|
5009
4585
|
const m = file.match(/^(.+)\.arb$/);
|
|
@@ -5044,7 +4620,7 @@ var init_flutter_arb2 = __esm({
|
|
|
5044
4620
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5045
4621
|
let data;
|
|
5046
4622
|
try {
|
|
5047
|
-
data = JSON.parse(
|
|
4623
|
+
data = JSON.parse(readFileSync14(join9(localeRoot, file), "utf8"));
|
|
5048
4624
|
} catch (e) {
|
|
5049
4625
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
5050
4626
|
continue;
|
|
@@ -5071,7 +4647,7 @@ var init_flutter_arb2 = __esm({
|
|
|
5071
4647
|
});
|
|
5072
4648
|
|
|
5073
4649
|
// src/server/import/parsers/apple-strings.ts
|
|
5074
|
-
import { readdirSync as readdirSync8, readFileSync as
|
|
4650
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync15, statSync as statSync5 } from "fs";
|
|
5075
4651
|
import { join as join10 } from "path";
|
|
5076
4652
|
function localeFromLproj(dir) {
|
|
5077
4653
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
@@ -5184,7 +4760,7 @@ var init_apple_strings2 = __esm({
|
|
|
5184
4760
|
let text;
|
|
5185
4761
|
try {
|
|
5186
4762
|
if (!statSync5(file).isFile()) continue;
|
|
5187
|
-
text =
|
|
4763
|
+
text = readFileSync15(file, "utf8");
|
|
5188
4764
|
} catch {
|
|
5189
4765
|
continue;
|
|
5190
4766
|
}
|
|
@@ -5204,7 +4780,7 @@ var init_apple_strings2 = __esm({
|
|
|
5204
4780
|
});
|
|
5205
4781
|
|
|
5206
4782
|
// src/server/import/parsers/angular-xliff.ts
|
|
5207
|
-
import { readdirSync as readdirSync9, readFileSync as
|
|
4783
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync16 } from "fs";
|
|
5208
4784
|
import { join as join11 } from "path";
|
|
5209
4785
|
function decodeEntities(s) {
|
|
5210
4786
|
return s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))).replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
@@ -5214,6 +4790,23 @@ function parseAttrs(s) {
|
|
|
5214
4790
|
for (const m of s.matchAll(/([\w-]+)="([^"]*)"/g)) out[m[1]] = decodeEntities(m[2]);
|
|
5215
4791
|
return out;
|
|
5216
4792
|
}
|
|
4793
|
+
function decodeLocations(body) {
|
|
4794
|
+
const out = [];
|
|
4795
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4796
|
+
for (const g of body.matchAll(/<context-group\b[^>]*\bpurpose="location"[^>]*>([\s\S]*?)<\/context-group>/g)) {
|
|
4797
|
+
const inner = g[1];
|
|
4798
|
+
const file = inner.match(/<context\b[^>]*context-type="sourcefile"[^>]*>([\s\S]*?)<\/context>/)?.[1];
|
|
4799
|
+
if (file === void 0) continue;
|
|
4800
|
+
const lineRaw = inner.match(/<context\b[^>]*context-type="linenumber"[^>]*>([\s\S]*?)<\/context>/)?.[1];
|
|
4801
|
+
const decodedFile = decodeEntities(file.trim());
|
|
4802
|
+
const line = lineRaw ? parseInt(lineRaw.trim(), 10) || 1 : 1;
|
|
4803
|
+
const dedup = `${decodedFile}:${line}`;
|
|
4804
|
+
if (seen.has(dedup)) continue;
|
|
4805
|
+
seen.add(dedup);
|
|
4806
|
+
out.push({ file: decodedFile, line });
|
|
4807
|
+
}
|
|
4808
|
+
return out;
|
|
4809
|
+
}
|
|
5217
4810
|
function decodeInline(raw, addMeta) {
|
|
5218
4811
|
let out = "";
|
|
5219
4812
|
let last = 0;
|
|
@@ -5257,7 +4850,7 @@ var init_angular_xliff2 = __esm({
|
|
|
5257
4850
|
if (fnameLocale !== void 0 && !LOCALE_RE5.test(fnameLocale)) continue;
|
|
5258
4851
|
let xml;
|
|
5259
4852
|
try {
|
|
5260
|
-
xml =
|
|
4853
|
+
xml = readFileSync16(join11(localeRoot, file), "utf8");
|
|
5261
4854
|
} catch (e) {
|
|
5262
4855
|
warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
|
|
5263
4856
|
continue;
|
|
@@ -5287,9 +4880,263 @@ var init_angular_xliff2 = __esm({
|
|
|
5287
4880
|
entry.values[sourceLocale] = decodeInline(src[1], addMeta);
|
|
5288
4881
|
seen(sourceLocale);
|
|
5289
4882
|
}
|
|
5290
|
-
if (tgt && tgt[2] !== "" && targetLocale !== void 0) {
|
|
5291
|
-
entry.values[targetLocale] = decodeInline(tgt[2], addMeta);
|
|
5292
|
-
seen(targetLocale);
|
|
4883
|
+
if (tgt && tgt[2] !== "" && targetLocale !== void 0) {
|
|
4884
|
+
entry.values[targetLocale] = decodeInline(tgt[2], addMeta);
|
|
4885
|
+
seen(targetLocale);
|
|
4886
|
+
}
|
|
4887
|
+
if (entry.locations === void 0) {
|
|
4888
|
+
const locs = decodeLocations(body);
|
|
4889
|
+
if (locs.length) entry.locations = locs;
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4892
|
+
}
|
|
4893
|
+
return { locales, keys, warnings };
|
|
4894
|
+
}
|
|
4895
|
+
};
|
|
4896
|
+
}
|
|
4897
|
+
});
|
|
4898
|
+
|
|
4899
|
+
// src/server/import/parsers/gettext-po.ts
|
|
4900
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync17 } from "fs";
|
|
4901
|
+
import { join as join12 } from "path";
|
|
4902
|
+
function unescapePo(s) {
|
|
4903
|
+
return s.replace(
|
|
4904
|
+
/\\([\\"ntr])/g,
|
|
4905
|
+
(_, c) => c === "n" ? "\n" : c === "t" ? " " : c === "r" ? "\r" : c
|
|
4906
|
+
);
|
|
4907
|
+
}
|
|
4908
|
+
function parseEntries(text) {
|
|
4909
|
+
const entries = [];
|
|
4910
|
+
let cur = null;
|
|
4911
|
+
let append = null;
|
|
4912
|
+
const flush = () => {
|
|
4913
|
+
if (cur && cur.msgid !== void 0) entries.push(cur);
|
|
4914
|
+
cur = null;
|
|
4915
|
+
append = null;
|
|
4916
|
+
};
|
|
4917
|
+
for (const line of text.split("\n")) {
|
|
4918
|
+
if (line.trim() === "") {
|
|
4919
|
+
flush();
|
|
4920
|
+
continue;
|
|
4921
|
+
}
|
|
4922
|
+
if (line.startsWith("#")) continue;
|
|
4923
|
+
const m = line.match(DIRECTIVE_RE);
|
|
4924
|
+
if (m) {
|
|
4925
|
+
const kw = m[1];
|
|
4926
|
+
const idx = m[2];
|
|
4927
|
+
const body = unescapePo(m[3]);
|
|
4928
|
+
if (cur && (kw === "msgctxt" || kw === "msgid" && cur.msgid !== void 0)) flush();
|
|
4929
|
+
cur ??= { plurals: /* @__PURE__ */ new Map() };
|
|
4930
|
+
const entry = cur;
|
|
4931
|
+
if (kw === "msgctxt") {
|
|
4932
|
+
entry.msgctxt = body;
|
|
4933
|
+
append = (c) => {
|
|
4934
|
+
entry.msgctxt = (entry.msgctxt ?? "") + c;
|
|
4935
|
+
};
|
|
4936
|
+
} else if (kw === "msgid") {
|
|
4937
|
+
entry.msgid = body;
|
|
4938
|
+
append = (c) => {
|
|
4939
|
+
entry.msgid = (entry.msgid ?? "") + c;
|
|
4940
|
+
};
|
|
4941
|
+
} else if (kw === "msgid_plural") {
|
|
4942
|
+
entry.msgidPlural = body;
|
|
4943
|
+
append = (c) => {
|
|
4944
|
+
entry.msgidPlural = (entry.msgidPlural ?? "") + c;
|
|
4945
|
+
};
|
|
4946
|
+
} else if (idx !== void 0) {
|
|
4947
|
+
const i = Number(idx);
|
|
4948
|
+
entry.plurals.set(i, body);
|
|
4949
|
+
append = (c) => {
|
|
4950
|
+
entry.plurals.set(i, (entry.plurals.get(i) ?? "") + c);
|
|
4951
|
+
};
|
|
4952
|
+
} else {
|
|
4953
|
+
entry.msgstr = body;
|
|
4954
|
+
append = (c) => {
|
|
4955
|
+
entry.msgstr = (entry.msgstr ?? "") + c;
|
|
4956
|
+
};
|
|
4957
|
+
}
|
|
4958
|
+
continue;
|
|
4959
|
+
}
|
|
4960
|
+
const cont = line.match(CONT_RE);
|
|
4961
|
+
if (cont && append) append(unescapePo(cont[1]));
|
|
4962
|
+
}
|
|
4963
|
+
flush();
|
|
4964
|
+
return entries;
|
|
4965
|
+
}
|
|
4966
|
+
function discoverPoFiles(root) {
|
|
4967
|
+
const found = [];
|
|
4968
|
+
const entries = readdirSync10(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
4969
|
+
for (const e of entries) {
|
|
4970
|
+
if (e.isFile() && e.name.endsWith(".po")) {
|
|
4971
|
+
const base = e.name.slice(0, -3);
|
|
4972
|
+
found.push({ path: join12(root, e.name), rel: e.name, locale: LOCALE_RE6.test(base) ? base : null });
|
|
4973
|
+
} else if (e.isDirectory() && LOCALE_RE6.test(e.name)) {
|
|
4974
|
+
for (const sub of [join12(e.name, "LC_MESSAGES"), e.name]) {
|
|
4975
|
+
let names;
|
|
4976
|
+
try {
|
|
4977
|
+
names = readdirSync10(join12(root, sub)).sort();
|
|
4978
|
+
} catch {
|
|
4979
|
+
continue;
|
|
4980
|
+
}
|
|
4981
|
+
for (const f of names) {
|
|
4982
|
+
if (f.endsWith(".po")) found.push({ path: join12(root, sub, f), rel: join12(sub, f), locale: e.name });
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
4986
|
+
}
|
|
4987
|
+
return found;
|
|
4988
|
+
}
|
|
4989
|
+
var LOCALE_RE6, DIRECTIVE_RE, CONT_RE, gettextPo2;
|
|
4990
|
+
var init_gettext_po2 = __esm({
|
|
4991
|
+
"src/server/import/parsers/gettext-po.ts"() {
|
|
4992
|
+
"use strict";
|
|
4993
|
+
init_plurals();
|
|
4994
|
+
LOCALE_RE6 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4995
|
+
DIRECTIVE_RE = /^(msgctxt|msgid_plural|msgid|msgstr)(?:\[(\d+)\])?[ \t]+"(.*)"\s*$/;
|
|
4996
|
+
CONT_RE = /^[ \t]*"(.*)"\s*$/;
|
|
4997
|
+
gettextPo2 = {
|
|
4998
|
+
name: "gettext-po",
|
|
4999
|
+
parse(localeRoot, opts) {
|
|
5000
|
+
const warnings = [];
|
|
5001
|
+
const keys = {};
|
|
5002
|
+
const locales = [];
|
|
5003
|
+
for (const file of discoverPoFiles(localeRoot)) {
|
|
5004
|
+
let entries;
|
|
5005
|
+
try {
|
|
5006
|
+
entries = parseEntries(readFileSync17(file.path, "utf8"));
|
|
5007
|
+
} catch (e) {
|
|
5008
|
+
warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
|
|
5009
|
+
continue;
|
|
5010
|
+
}
|
|
5011
|
+
const header = entries.find((e) => e.msgid === "" && e.msgctxt === void 0);
|
|
5012
|
+
const headerLang = header?.msgstr?.match(/^Language:[ \t]*([A-Za-z0-9_-]+)/m)?.[1];
|
|
5013
|
+
const locale = file.locale ?? headerLang;
|
|
5014
|
+
if (!locale) {
|
|
5015
|
+
warnings.push(`gettext-po: cannot determine locale for ${file.rel}; skipped`);
|
|
5016
|
+
continue;
|
|
5017
|
+
}
|
|
5018
|
+
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5019
|
+
if (!locales.includes(locale)) locales.push(locale);
|
|
5020
|
+
const cats = categoriesFor(locale);
|
|
5021
|
+
for (const entry of entries) {
|
|
5022
|
+
if (entry === header) continue;
|
|
5023
|
+
const key = entry.msgctxt ?? entry.msgid;
|
|
5024
|
+
if (!key) continue;
|
|
5025
|
+
if (entry.msgidPlural !== void 0) {
|
|
5026
|
+
const forms = {};
|
|
5027
|
+
for (const [i, body] of [...entry.plurals].sort((a, b) => a[0] - b[0])) {
|
|
5028
|
+
if (body === "") continue;
|
|
5029
|
+
const cat = cats[i];
|
|
5030
|
+
if (!cat) {
|
|
5031
|
+
warnings.push(
|
|
5032
|
+
`gettext-po: ${file.rel} "${key}": msgstr[${i}] exceeds the ${cats.length} plural forms of "${locale}"; ignored`
|
|
5033
|
+
);
|
|
5034
|
+
continue;
|
|
5035
|
+
}
|
|
5036
|
+
forms[cat] = body.split("%d").join("{count}");
|
|
5037
|
+
}
|
|
5038
|
+
if (!forms.other) continue;
|
|
5039
|
+
(keys[key] ??= { values: {} }).values[locale] = formsToIcu("count", forms);
|
|
5040
|
+
} else {
|
|
5041
|
+
if (!entry.msgstr) continue;
|
|
5042
|
+
(keys[key] ??= { values: {} }).values[locale] = entry.msgstr;
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
return { locales, keys, warnings };
|
|
5047
|
+
}
|
|
5048
|
+
};
|
|
5049
|
+
}
|
|
5050
|
+
});
|
|
5051
|
+
|
|
5052
|
+
// src/server/import/parsers/i18next-json.ts
|
|
5053
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync18, statSync as statSync6 } from "fs";
|
|
5054
|
+
import { join as join13 } from "path";
|
|
5055
|
+
function safeIsDir2(p) {
|
|
5056
|
+
try {
|
|
5057
|
+
return statSync6(p).isDirectory();
|
|
5058
|
+
} catch {
|
|
5059
|
+
return false;
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
function fromI18next(value) {
|
|
5063
|
+
if (isIcuPluralOrSelect(value)) return value;
|
|
5064
|
+
return value.replace(/\{\{(\w+)\}\}/g, "{$1}");
|
|
5065
|
+
}
|
|
5066
|
+
function ingestFile(path, label, prefix, locale, keys, warnings) {
|
|
5067
|
+
let data;
|
|
5068
|
+
try {
|
|
5069
|
+
data = JSON.parse(readFileSync18(path, "utf8"));
|
|
5070
|
+
} catch (e) {
|
|
5071
|
+
warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
|
|
5072
|
+
return false;
|
|
5073
|
+
}
|
|
5074
|
+
const fileWarnings = [];
|
|
5075
|
+
const flat = flattenObject(data, "", fileWarnings);
|
|
5076
|
+
for (const w of fileWarnings) warnings.push(`i18next-json: ${label}: ${w}`);
|
|
5077
|
+
const families = /* @__PURE__ */ new Set();
|
|
5078
|
+
for (const [k, v] of Object.entries(flat)) {
|
|
5079
|
+
const m = PLURAL_SUFFIX_RE.exec(k);
|
|
5080
|
+
if (m && m[2] === "other" && v !== "") families.add(m[1]);
|
|
5081
|
+
}
|
|
5082
|
+
const pluralForms = {};
|
|
5083
|
+
for (const [k, raw] of Object.entries(flat)) {
|
|
5084
|
+
if (raw === "") continue;
|
|
5085
|
+
const value = fromI18next(raw);
|
|
5086
|
+
const m = PLURAL_SUFFIX_RE.exec(k);
|
|
5087
|
+
if (m && families.has(m[1])) {
|
|
5088
|
+
(pluralForms[m[1]] ??= {})[m[2]] = value;
|
|
5089
|
+
continue;
|
|
5090
|
+
}
|
|
5091
|
+
if (families.has(k)) {
|
|
5092
|
+
warnings.push(
|
|
5093
|
+
`i18next-json: ${label}: key "${k}" collides with its own plural suffix family; the plural wins`
|
|
5094
|
+
);
|
|
5095
|
+
continue;
|
|
5096
|
+
}
|
|
5097
|
+
(keys[prefix + k] ??= { values: {} }).values[locale] = value;
|
|
5098
|
+
}
|
|
5099
|
+
for (const [base, forms] of Object.entries(pluralForms)) {
|
|
5100
|
+
(keys[prefix + base] ??= { values: {} }).values[locale] = formsToIcu(PLURAL_ARG, forms);
|
|
5101
|
+
}
|
|
5102
|
+
return true;
|
|
5103
|
+
}
|
|
5104
|
+
var LOCALE_RE7, PLURAL_SUFFIX_RE, PLURAL_ARG, DEFAULT_NAMESPACE, i18nextJson2;
|
|
5105
|
+
var init_i18next_json2 = __esm({
|
|
5106
|
+
"src/server/import/parsers/i18next-json.ts"() {
|
|
5107
|
+
"use strict";
|
|
5108
|
+
init_flatten();
|
|
5109
|
+
init_plurals();
|
|
5110
|
+
init_placeholders();
|
|
5111
|
+
LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5112
|
+
PLURAL_SUFFIX_RE = /^(.+)_(zero|one|two|few|many|other)$/;
|
|
5113
|
+
PLURAL_ARG = "count";
|
|
5114
|
+
DEFAULT_NAMESPACE = "translation";
|
|
5115
|
+
i18nextJson2 = {
|
|
5116
|
+
name: "i18next-json",
|
|
5117
|
+
parse(localeRoot, opts) {
|
|
5118
|
+
const warnings = [];
|
|
5119
|
+
const keys = {};
|
|
5120
|
+
const locales = [];
|
|
5121
|
+
for (const entry of readdirSync11(localeRoot).sort()) {
|
|
5122
|
+
const full = join13(localeRoot, entry);
|
|
5123
|
+
if (safeIsDir2(full)) {
|
|
5124
|
+
if (!LOCALE_RE7.test(entry)) continue;
|
|
5125
|
+
if (opts?.locales && !opts.locales.includes(entry)) continue;
|
|
5126
|
+
let any = false;
|
|
5127
|
+
for (const file of readdirSync11(full).sort()) {
|
|
5128
|
+
if (!file.endsWith(".json")) continue;
|
|
5129
|
+
const ns = file.slice(0, -".json".length);
|
|
5130
|
+
const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
|
|
5131
|
+
if (ingestFile(join13(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
|
|
5132
|
+
}
|
|
5133
|
+
if (any && !locales.includes(entry)) locales.push(entry);
|
|
5134
|
+
} else if (entry.endsWith(".json")) {
|
|
5135
|
+
const locale = entry.slice(0, -".json".length);
|
|
5136
|
+
if (!LOCALE_RE7.test(locale)) continue;
|
|
5137
|
+
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5138
|
+
if (ingestFile(full, entry, "", locale, keys, warnings) && !locales.includes(locale)) {
|
|
5139
|
+
locales.push(locale);
|
|
5293
5140
|
}
|
|
5294
5141
|
}
|
|
5295
5142
|
}
|
|
@@ -5299,151 +5146,239 @@ var init_angular_xliff2 = __esm({
|
|
|
5299
5146
|
}
|
|
5300
5147
|
});
|
|
5301
5148
|
|
|
5302
|
-
// src/server/import/parsers/
|
|
5303
|
-
import { readdirSync as
|
|
5304
|
-
import { join as
|
|
5305
|
-
function
|
|
5306
|
-
return
|
|
5307
|
-
/\\([\\"ntr])/g,
|
|
5308
|
-
(_, c) => c === "n" ? "\n" : c === "t" ? " " : c === "r" ? "\r" : c
|
|
5309
|
-
);
|
|
5149
|
+
// src/server/import/parsers/rails-yaml.ts
|
|
5150
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync19 } from "fs";
|
|
5151
|
+
import { join as join14 } from "path";
|
|
5152
|
+
function fromRuby(value) {
|
|
5153
|
+
return value.replace(/%\{(\w+)\}/g, "{$1}");
|
|
5310
5154
|
}
|
|
5311
|
-
function
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
for (const line of text.split("\n")) {
|
|
5321
|
-
if (line.trim() === "") {
|
|
5322
|
-
flush();
|
|
5155
|
+
function makeNode() {
|
|
5156
|
+
return /* @__PURE__ */ Object.create(null);
|
|
5157
|
+
}
|
|
5158
|
+
function decodeDouble(body) {
|
|
5159
|
+
let out = "";
|
|
5160
|
+
for (let i = 0; i < body.length; i++) {
|
|
5161
|
+
const c = body[i];
|
|
5162
|
+
if (c !== "\\") {
|
|
5163
|
+
out += c;
|
|
5323
5164
|
continue;
|
|
5324
5165
|
}
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
append = (c) => {
|
|
5347
|
-
entry.msgidPlural = (entry.msgidPlural ?? "") + c;
|
|
5348
|
-
};
|
|
5349
|
-
} else if (idx !== void 0) {
|
|
5350
|
-
const i = Number(idx);
|
|
5351
|
-
entry.plurals.set(i, body);
|
|
5352
|
-
append = (c) => {
|
|
5353
|
-
entry.plurals.set(i, (entry.plurals.get(i) ?? "") + c);
|
|
5354
|
-
};
|
|
5166
|
+
const n = body[++i];
|
|
5167
|
+
if (n === void 0) break;
|
|
5168
|
+
out += n === "n" ? "\n" : n === "r" ? "\r" : n === "t" ? " " : n;
|
|
5169
|
+
}
|
|
5170
|
+
return out;
|
|
5171
|
+
}
|
|
5172
|
+
function scanQuoted(s, start) {
|
|
5173
|
+
const q = s[start];
|
|
5174
|
+
if (q === '"') {
|
|
5175
|
+
for (let i = start + 1; i < s.length; i++) {
|
|
5176
|
+
if (s[i] === "\\") i++;
|
|
5177
|
+
else if (s[i] === '"') return { text: decodeDouble(s.slice(start + 1, i)), end: i + 1 };
|
|
5178
|
+
}
|
|
5179
|
+
return null;
|
|
5180
|
+
}
|
|
5181
|
+
let out = "";
|
|
5182
|
+
for (let i = start + 1; i < s.length; i++) {
|
|
5183
|
+
if (s[i] === "'") {
|
|
5184
|
+
if (s[i + 1] === "'") {
|
|
5185
|
+
out += "'";
|
|
5186
|
+
i++;
|
|
5355
5187
|
} else {
|
|
5356
|
-
|
|
5357
|
-
append = (c) => {
|
|
5358
|
-
entry.msgstr = (entry.msgstr ?? "") + c;
|
|
5359
|
-
};
|
|
5188
|
+
return { text: out, end: i + 1 };
|
|
5360
5189
|
}
|
|
5190
|
+
} else {
|
|
5191
|
+
out += s[i];
|
|
5192
|
+
}
|
|
5193
|
+
}
|
|
5194
|
+
return null;
|
|
5195
|
+
}
|
|
5196
|
+
function stripPlainComment(s) {
|
|
5197
|
+
const m = /(^|\s)#/.exec(s);
|
|
5198
|
+
return (m && m.index >= 0 ? s.slice(0, m.index) : s).trim();
|
|
5199
|
+
}
|
|
5200
|
+
function onlyTrailing(s) {
|
|
5201
|
+
return /^\s*(#.*)?$/.test(s);
|
|
5202
|
+
}
|
|
5203
|
+
function parseYamlSubset(text, file, warnings) {
|
|
5204
|
+
const roots = {};
|
|
5205
|
+
const lines = text.split(/\r?\n/);
|
|
5206
|
+
let stack = [];
|
|
5207
|
+
let skipDeeperThan = null;
|
|
5208
|
+
let lastLeafIndent = null;
|
|
5209
|
+
for (let n = 0; n < lines.length; n++) {
|
|
5210
|
+
const raw = lines[n];
|
|
5211
|
+
const lineNo = n + 1;
|
|
5212
|
+
if (raw.trim() === "" || raw.trim().startsWith("#")) continue;
|
|
5213
|
+
if (raw.trim() === "---") continue;
|
|
5214
|
+
const indentMatch = /^[ \t]*/.exec(raw)[0];
|
|
5215
|
+
if (indentMatch.includes(" ")) {
|
|
5216
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: tab in indentation; line skipped`);
|
|
5361
5217
|
continue;
|
|
5362
5218
|
}
|
|
5363
|
-
const
|
|
5364
|
-
if (
|
|
5219
|
+
const indent = indentMatch.length;
|
|
5220
|
+
if (skipDeeperThan !== null) {
|
|
5221
|
+
if (indent > skipDeeperThan) continue;
|
|
5222
|
+
skipDeeperThan = null;
|
|
5223
|
+
}
|
|
5224
|
+
const content = raw.slice(indent);
|
|
5225
|
+
if (content.startsWith("- ") || content === "-") {
|
|
5226
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: sequences are not supported; node skipped`);
|
|
5227
|
+
skipDeeperThan = indent;
|
|
5228
|
+
continue;
|
|
5229
|
+
}
|
|
5230
|
+
let key;
|
|
5231
|
+
let rest;
|
|
5232
|
+
if (content[0] === '"' || content[0] === "'") {
|
|
5233
|
+
const k = scanQuoted(content, 0);
|
|
5234
|
+
if (!k || content[k.end] !== ":") {
|
|
5235
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: unparseable quoted key; line skipped`);
|
|
5236
|
+
skipDeeperThan = indent;
|
|
5237
|
+
continue;
|
|
5238
|
+
}
|
|
5239
|
+
key = k.text;
|
|
5240
|
+
rest = content.slice(k.end + 1);
|
|
5241
|
+
} else {
|
|
5242
|
+
const m = /^(.*?):(?=\s|$)/.exec(content);
|
|
5243
|
+
if (!m) {
|
|
5244
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: not a "key: value" mapping line; line skipped`);
|
|
5245
|
+
skipDeeperThan = indent;
|
|
5246
|
+
continue;
|
|
5247
|
+
}
|
|
5248
|
+
key = m[1].trim();
|
|
5249
|
+
rest = content.slice(m[0].length);
|
|
5250
|
+
}
|
|
5251
|
+
if (lastLeafIndent !== null && indent > lastLeafIndent) {
|
|
5252
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: unexpected indentation under a scalar; line skipped`);
|
|
5253
|
+
skipDeeperThan = indent - 1;
|
|
5254
|
+
continue;
|
|
5255
|
+
}
|
|
5256
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) stack.pop();
|
|
5257
|
+
const trimmed = rest.trim();
|
|
5258
|
+
let value;
|
|
5259
|
+
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
5260
|
+
value = null;
|
|
5261
|
+
} else if (trimmed[0] === "&" || trimmed[0] === "*") {
|
|
5262
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: YAML anchors/aliases are not supported; node skipped`);
|
|
5263
|
+
skipDeeperThan = indent;
|
|
5264
|
+
continue;
|
|
5265
|
+
} else if (trimmed[0] === "|" || trimmed[0] === ">") {
|
|
5266
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: block scalars are not supported; node skipped`);
|
|
5267
|
+
skipDeeperThan = indent;
|
|
5268
|
+
continue;
|
|
5269
|
+
} else if (trimmed[0] === "[" || trimmed[0] === "{") {
|
|
5270
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: flow collections are not supported; node skipped`);
|
|
5271
|
+
skipDeeperThan = indent;
|
|
5272
|
+
continue;
|
|
5273
|
+
} else if (trimmed[0] === '"' || trimmed[0] === "'") {
|
|
5274
|
+
const v = scanQuoted(trimmed, 0);
|
|
5275
|
+
if (!v || !onlyTrailing(trimmed.slice(v.end))) {
|
|
5276
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: unterminated or trailing-garbage quoted value; line skipped`);
|
|
5277
|
+
continue;
|
|
5278
|
+
}
|
|
5279
|
+
value = v.text;
|
|
5280
|
+
} else {
|
|
5281
|
+
value = stripPlainComment(trimmed);
|
|
5282
|
+
}
|
|
5283
|
+
if (stack.length === 0) {
|
|
5284
|
+
if (value !== null) {
|
|
5285
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: top-level key "${key}" has a scalar value; skipped`);
|
|
5286
|
+
lastLeafIndent = indent;
|
|
5287
|
+
continue;
|
|
5288
|
+
}
|
|
5289
|
+
const root = roots[key] ??= makeNode();
|
|
5290
|
+
stack = [{ indent, node: root }];
|
|
5291
|
+
lastLeafIndent = null;
|
|
5292
|
+
continue;
|
|
5293
|
+
}
|
|
5294
|
+
const parent = stack[stack.length - 1].node;
|
|
5295
|
+
if (key in parent) {
|
|
5296
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: duplicate key "${key}"; later value wins`);
|
|
5297
|
+
}
|
|
5298
|
+
if (value === null) {
|
|
5299
|
+
const child = makeNode();
|
|
5300
|
+
parent[key] = child;
|
|
5301
|
+
stack.push({ indent, node: child });
|
|
5302
|
+
lastLeafIndent = null;
|
|
5303
|
+
} else {
|
|
5304
|
+
parent[key] = value;
|
|
5305
|
+
lastLeafIndent = indent;
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
5308
|
+
return { roots };
|
|
5309
|
+
}
|
|
5310
|
+
function asPluralForms(node) {
|
|
5311
|
+
const entries = Object.entries(node);
|
|
5312
|
+
if (entries.length === 0) return null;
|
|
5313
|
+
const forms = {};
|
|
5314
|
+
for (const [k, v] of entries) {
|
|
5315
|
+
if (!CATEGORY_SET.has(k) || typeof v !== "string") return null;
|
|
5316
|
+
if (v !== "") forms[k] = v;
|
|
5365
5317
|
}
|
|
5366
|
-
|
|
5367
|
-
return
|
|
5318
|
+
if (!("other" in forms)) return null;
|
|
5319
|
+
return forms;
|
|
5368
5320
|
}
|
|
5369
|
-
function
|
|
5370
|
-
const
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
if (
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
let names;
|
|
5379
|
-
try {
|
|
5380
|
-
names = readdirSync10(join12(root, sub)).sort();
|
|
5381
|
-
} catch {
|
|
5382
|
-
continue;
|
|
5383
|
-
}
|
|
5384
|
-
for (const f of names) {
|
|
5385
|
-
if (f.endsWith(".po")) found.push({ path: join12(root, sub, f), rel: join12(sub, f), locale: e.name });
|
|
5386
|
-
}
|
|
5387
|
-
}
|
|
5321
|
+
function synthesizeIcu(forms, file, key, warnings) {
|
|
5322
|
+
const parts = [];
|
|
5323
|
+
for (const cat of PLURAL_CATEGORIES) {
|
|
5324
|
+
const body = forms[cat];
|
|
5325
|
+
if (body === void 0) continue;
|
|
5326
|
+
if (body.includes("#")) {
|
|
5327
|
+
warnings.push(
|
|
5328
|
+
`rails-yaml: ${file}: plural "${key}" form "${cat}" contains "#", which ICU reads as the count placeholder`
|
|
5329
|
+
);
|
|
5388
5330
|
}
|
|
5331
|
+
parts.push(`${cat} {${fromRuby(body)}}`);
|
|
5389
5332
|
}
|
|
5390
|
-
return
|
|
5333
|
+
return `{count, plural, ${parts.join(" ")}}`;
|
|
5391
5334
|
}
|
|
5392
|
-
var
|
|
5393
|
-
var
|
|
5394
|
-
"src/server/import/parsers/
|
|
5335
|
+
var LOCALE_RE8, CATEGORY_SET, railsYaml2;
|
|
5336
|
+
var init_rails_yaml2 = __esm({
|
|
5337
|
+
"src/server/import/parsers/rails-yaml.ts"() {
|
|
5395
5338
|
"use strict";
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
name: "gettext-po",
|
|
5339
|
+
init_schema();
|
|
5340
|
+
LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
|
|
5341
|
+
CATEGORY_SET = new Set(PLURAL_CATEGORIES);
|
|
5342
|
+
railsYaml2 = {
|
|
5343
|
+
name: "rails-yaml",
|
|
5402
5344
|
parse(localeRoot, opts) {
|
|
5403
5345
|
const warnings = [];
|
|
5404
5346
|
const keys = {};
|
|
5405
5347
|
const locales = [];
|
|
5406
|
-
|
|
5407
|
-
|
|
5348
|
+
const wanted = opts?.locales?.map((l) => l.toLowerCase());
|
|
5349
|
+
const addValue = (key, locale, value) => {
|
|
5350
|
+
(keys[key] ??= { values: {} }).values[locale] = value;
|
|
5351
|
+
};
|
|
5352
|
+
const flatten = (node, prefix, locale, file) => {
|
|
5353
|
+
for (const [k, v] of Object.entries(node)) {
|
|
5354
|
+
const key = prefix ? `${prefix}.${k}` : k;
|
|
5355
|
+
if (typeof v === "string") {
|
|
5356
|
+
if (v !== "") addValue(key, locale, fromRuby(v));
|
|
5357
|
+
continue;
|
|
5358
|
+
}
|
|
5359
|
+
const forms = asPluralForms(v);
|
|
5360
|
+
if (forms) addValue(key, locale, synthesizeIcu(forms, file, key, warnings));
|
|
5361
|
+
else flatten(v, key, locale, file);
|
|
5362
|
+
}
|
|
5363
|
+
};
|
|
5364
|
+
for (const file of readdirSync12(localeRoot).sort()) {
|
|
5365
|
+
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
5366
|
+
let text;
|
|
5408
5367
|
try {
|
|
5409
|
-
|
|
5368
|
+
text = readFileSync19(join14(localeRoot, file), "utf8");
|
|
5410
5369
|
} catch (e) {
|
|
5411
|
-
warnings.push(`
|
|
5412
|
-
continue;
|
|
5413
|
-
}
|
|
5414
|
-
const header = entries.find((e) => e.msgid === "" && e.msgctxt === void 0);
|
|
5415
|
-
const headerLang = header?.msgstr?.match(/^Language:[ \t]*([A-Za-z0-9_-]+)/m)?.[1];
|
|
5416
|
-
const locale = file.locale ?? headerLang;
|
|
5417
|
-
if (!locale) {
|
|
5418
|
-
warnings.push(`gettext-po: cannot determine locale for ${file.rel}; skipped`);
|
|
5370
|
+
warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
|
|
5419
5371
|
continue;
|
|
5420
5372
|
}
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
const key = entry.msgctxt ?? entry.msgid;
|
|
5427
|
-
if (!key) continue;
|
|
5428
|
-
if (entry.msgidPlural !== void 0) {
|
|
5429
|
-
const forms = {};
|
|
5430
|
-
for (const [i, body] of [...entry.plurals].sort((a, b) => a[0] - b[0])) {
|
|
5431
|
-
if (body === "") continue;
|
|
5432
|
-
const cat = cats[i];
|
|
5433
|
-
if (!cat) {
|
|
5434
|
-
warnings.push(
|
|
5435
|
-
`gettext-po: ${file.rel} "${key}": msgstr[${i}] exceeds the ${cats.length} plural forms of "${locale}"; ignored`
|
|
5436
|
-
);
|
|
5437
|
-
continue;
|
|
5438
|
-
}
|
|
5439
|
-
forms[cat] = body.split("%d").join("{count}");
|
|
5440
|
-
}
|
|
5441
|
-
if (!forms.other) continue;
|
|
5442
|
-
(keys[key] ??= { values: {} }).values[locale] = formsToIcu("count", forms);
|
|
5443
|
-
} else {
|
|
5444
|
-
if (!entry.msgstr) continue;
|
|
5445
|
-
(keys[key] ??= { values: {} }).values[locale] = entry.msgstr;
|
|
5373
|
+
const { roots } = parseYamlSubset(text, file, warnings);
|
|
5374
|
+
for (const token of Object.keys(roots).sort()) {
|
|
5375
|
+
if (!LOCALE_RE8.test(token)) {
|
|
5376
|
+
warnings.push(`rails-yaml: ${file}: top-level key "${token}" is not a locale; subtree skipped`);
|
|
5377
|
+
continue;
|
|
5446
5378
|
}
|
|
5379
|
+
if (wanted && !wanted.includes(token.toLowerCase())) continue;
|
|
5380
|
+
if (!locales.includes(token)) locales.push(token);
|
|
5381
|
+
flatten(roots[token], "", token, file);
|
|
5447
5382
|
}
|
|
5448
5383
|
}
|
|
5449
5384
|
return { locales, keys, warnings };
|
|
@@ -5452,95 +5387,178 @@ var init_gettext_po2 = __esm({
|
|
|
5452
5387
|
}
|
|
5453
5388
|
});
|
|
5454
5389
|
|
|
5455
|
-
// src/server/import/parsers/
|
|
5456
|
-
import { readdirSync as
|
|
5457
|
-
import { join as
|
|
5458
|
-
function
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
return false;
|
|
5463
|
-
}
|
|
5390
|
+
// src/server/import/parsers/apple-stringsdict.ts
|
|
5391
|
+
import { readdirSync as readdirSync13, readFileSync as readFileSync20, statSync as statSync7 } from "fs";
|
|
5392
|
+
import { join as join15 } from "path";
|
|
5393
|
+
function localeFromLproj2(dir) {
|
|
5394
|
+
const m = dir.match(/^(.+)\.lproj$/);
|
|
5395
|
+
if (!m) return null;
|
|
5396
|
+
return LOCALE_RE9.test(m[1]) ? m[1] : null;
|
|
5464
5397
|
}
|
|
5465
|
-
function
|
|
5466
|
-
|
|
5467
|
-
return value.replace(/\{\{(\w+)\}\}/g, "{$1}");
|
|
5398
|
+
function decodeEntities2(s) {
|
|
5399
|
+
return s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))).replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
5468
5400
|
}
|
|
5469
|
-
function
|
|
5470
|
-
let
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
const value = fromI18next(raw);
|
|
5489
|
-
const m = PLURAL_SUFFIX_RE.exec(k);
|
|
5490
|
-
if (m && families.has(m[1])) {
|
|
5491
|
-
(pluralForms[m[1]] ??= {})[m[2]] = value;
|
|
5492
|
-
continue;
|
|
5401
|
+
function parsePlistDict(xml) {
|
|
5402
|
+
let i = 0;
|
|
5403
|
+
const n = xml.length;
|
|
5404
|
+
const skipTrivia = () => {
|
|
5405
|
+
for (; ; ) {
|
|
5406
|
+
while (i < n && /\s/.test(xml[i])) i++;
|
|
5407
|
+
if (xml.startsWith("<!--", i)) {
|
|
5408
|
+
const end = xml.indexOf("-->", i + 4);
|
|
5409
|
+
if (end === -1) throw new Error("unterminated comment");
|
|
5410
|
+
i = end + 3;
|
|
5411
|
+
continue;
|
|
5412
|
+
}
|
|
5413
|
+
if (xml.startsWith("<?", i) || xml.startsWith("<!", i) && !xml.startsWith("<!--", i)) {
|
|
5414
|
+
const end = xml.indexOf(">", i);
|
|
5415
|
+
if (end === -1) throw new Error("unterminated declaration");
|
|
5416
|
+
i = end + 1;
|
|
5417
|
+
continue;
|
|
5418
|
+
}
|
|
5419
|
+
break;
|
|
5493
5420
|
}
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5421
|
+
};
|
|
5422
|
+
const readTag = () => {
|
|
5423
|
+
if (xml[i] !== "<") throw new Error(`expected a tag at offset ${i}`);
|
|
5424
|
+
const end = xml.indexOf(">", i);
|
|
5425
|
+
if (end === -1) throw new Error("unterminated tag");
|
|
5426
|
+
let body = xml.slice(i + 1, end).trim();
|
|
5427
|
+
i = end + 1;
|
|
5428
|
+
const closing = body.startsWith("/");
|
|
5429
|
+
if (closing) body = body.slice(1).trim();
|
|
5430
|
+
const selfClosing = body.endsWith("/");
|
|
5431
|
+
if (selfClosing) body = body.slice(0, -1).trim();
|
|
5432
|
+
const name = body.split(/\s/)[0];
|
|
5433
|
+
if (!name) throw new Error(`empty tag at offset ${end}`);
|
|
5434
|
+
return { name, closing, selfClosing };
|
|
5435
|
+
};
|
|
5436
|
+
const readElementText = (name) => {
|
|
5437
|
+
const re = new RegExp(`</${name}\\s*>`, "g");
|
|
5438
|
+
re.lastIndex = i;
|
|
5439
|
+
const m = re.exec(xml);
|
|
5440
|
+
if (!m) throw new Error(`unterminated <${name}>`);
|
|
5441
|
+
const text = xml.slice(i, m.index);
|
|
5442
|
+
i = m.index + m[0].length;
|
|
5443
|
+
return decodeEntities2(text);
|
|
5444
|
+
};
|
|
5445
|
+
const readValue = (tag2) => {
|
|
5446
|
+
if (tag2.name === "dict") return tag2.selfClosing ? {} : readDict();
|
|
5447
|
+
if (tag2.name === "true" || tag2.name === "false") {
|
|
5448
|
+
if (!tag2.selfClosing) readElementText(tag2.name);
|
|
5449
|
+
return tag2.name;
|
|
5499
5450
|
}
|
|
5500
|
-
(
|
|
5451
|
+
if (["string", "integer", "real", "date", "data"].includes(tag2.name)) {
|
|
5452
|
+
return tag2.selfClosing ? "" : readElementText(tag2.name);
|
|
5453
|
+
}
|
|
5454
|
+
throw new Error(`unsupported plist element <${tag2.name}>`);
|
|
5455
|
+
};
|
|
5456
|
+
const readDict = () => {
|
|
5457
|
+
const out = {};
|
|
5458
|
+
for (; ; ) {
|
|
5459
|
+
skipTrivia();
|
|
5460
|
+
const tag2 = readTag();
|
|
5461
|
+
if (tag2.closing) {
|
|
5462
|
+
if (tag2.name !== "dict") throw new Error(`unexpected </${tag2.name}> inside <dict>`);
|
|
5463
|
+
return out;
|
|
5464
|
+
}
|
|
5465
|
+
if (tag2.name !== "key") throw new Error(`expected <key> inside <dict>, got <${tag2.name}>`);
|
|
5466
|
+
const key = readElementText("key");
|
|
5467
|
+
skipTrivia();
|
|
5468
|
+
const vt = readTag();
|
|
5469
|
+
if (vt.closing) throw new Error(`<key>${key}</key> has no value`);
|
|
5470
|
+
out[key] = readValue(vt);
|
|
5471
|
+
}
|
|
5472
|
+
};
|
|
5473
|
+
skipTrivia();
|
|
5474
|
+
let tag = readTag();
|
|
5475
|
+
if (tag.name === "plist" && !tag.closing && !tag.selfClosing) {
|
|
5476
|
+
skipTrivia();
|
|
5477
|
+
tag = readTag();
|
|
5478
|
+
}
|
|
5479
|
+
if (tag.name !== "dict" || tag.closing) throw new Error("expected a root <dict>");
|
|
5480
|
+
return tag.selfClosing ? {} : readDict();
|
|
5481
|
+
}
|
|
5482
|
+
function entryToIcu(key, entry, file, warnings) {
|
|
5483
|
+
const warn = (msg) => {
|
|
5484
|
+
warnings.push(`apple-stringsdict: ${file}: key "${key}": ${msg}`);
|
|
5485
|
+
return null;
|
|
5486
|
+
};
|
|
5487
|
+
if (typeof entry !== "object") return warn("value is not a dict; skipped");
|
|
5488
|
+
const fmt = entry["NSStringLocalizedFormatKey"];
|
|
5489
|
+
if (typeof fmt !== "string") return warn("missing NSStringLocalizedFormatKey; skipped");
|
|
5490
|
+
const vars = [...fmt.matchAll(VAR_RE)];
|
|
5491
|
+
if (vars.length !== 1) {
|
|
5492
|
+
return warn(`format key has ${vars.length} %#@\u2026@ variables; only exactly one is supported; skipped`);
|
|
5501
5493
|
}
|
|
5502
|
-
|
|
5503
|
-
|
|
5494
|
+
const arg = vars[0][1];
|
|
5495
|
+
if (!/^\w+$/.test(arg)) return warn(`variable name "${arg}" is not a valid ICU argument; skipped`);
|
|
5496
|
+
const prefix = fmt.slice(0, vars[0].index);
|
|
5497
|
+
const suffix = fmt.slice(vars[0].index + vars[0][0].length);
|
|
5498
|
+
const varDict = entry[arg];
|
|
5499
|
+
if (typeof varDict !== "object") return warn(`variable "${arg}" has no dict; skipped`);
|
|
5500
|
+
const specType = varDict["NSStringFormatSpecTypeKey"];
|
|
5501
|
+
if (specType !== void 0 && specType !== "NSStringPluralRuleType") {
|
|
5502
|
+
return warn(`variable "${arg}" is not a plural rule (${String(specType)}); skipped`);
|
|
5504
5503
|
}
|
|
5505
|
-
|
|
5504
|
+
const valueType = varDict["NSStringFormatValueTypeKey"];
|
|
5505
|
+
const token = `%${typeof valueType === "string" && valueType ? valueType : "d"}`;
|
|
5506
|
+
const forms = {};
|
|
5507
|
+
for (const cat of PLURAL_CATEGORIES) {
|
|
5508
|
+
const body = varDict[cat];
|
|
5509
|
+
if (typeof body !== "string") continue;
|
|
5510
|
+
forms[cat] = prefix + body.split(token).join(`{${arg}}`) + suffix;
|
|
5511
|
+
}
|
|
5512
|
+
if (forms.other === void 0) return warn(`variable "${arg}" has no "other" form; skipped`);
|
|
5513
|
+
return formsToIcu(arg, forms);
|
|
5506
5514
|
}
|
|
5507
|
-
var
|
|
5508
|
-
var
|
|
5509
|
-
"src/server/import/parsers/
|
|
5515
|
+
var LOCALE_RE9, TABLE2, VAR_RE, appleStringsdict2;
|
|
5516
|
+
var init_apple_stringsdict2 = __esm({
|
|
5517
|
+
"src/server/import/parsers/apple-stringsdict.ts"() {
|
|
5510
5518
|
"use strict";
|
|
5511
|
-
|
|
5519
|
+
init_schema();
|
|
5512
5520
|
init_plurals();
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
i18nextJson2 = {
|
|
5519
|
-
name: "i18next-json",
|
|
5521
|
+
LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5522
|
+
TABLE2 = "Localizable.stringsdict";
|
|
5523
|
+
VAR_RE = /%#@([^@]*)@/g;
|
|
5524
|
+
appleStringsdict2 = {
|
|
5525
|
+
name: "apple-stringsdict",
|
|
5520
5526
|
parse(localeRoot, opts) {
|
|
5521
5527
|
const warnings = [];
|
|
5522
5528
|
const keys = {};
|
|
5523
5529
|
const locales = [];
|
|
5524
|
-
for (const
|
|
5525
|
-
const
|
|
5526
|
-
if (
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5530
|
+
for (const dir of readdirSync13(localeRoot).sort()) {
|
|
5531
|
+
const locale = localeFromLproj2(dir);
|
|
5532
|
+
if (!locale) continue;
|
|
5533
|
+
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5534
|
+
const file = join15(localeRoot, dir, TABLE2);
|
|
5535
|
+
let text;
|
|
5536
|
+
try {
|
|
5537
|
+
if (!statSync7(file).isFile()) continue;
|
|
5538
|
+
text = readFileSync20(file, "utf8");
|
|
5539
|
+
} catch {
|
|
5540
|
+
continue;
|
|
5541
|
+
}
|
|
5542
|
+
locales.push(locale);
|
|
5543
|
+
const others = readdirSync13(join15(localeRoot, dir)).filter(
|
|
5544
|
+
(f) => f.endsWith(".stringsdict") && f !== TABLE2
|
|
5545
|
+
);
|
|
5546
|
+
if (others.length) {
|
|
5547
|
+
warnings.push(
|
|
5548
|
+
`apple-stringsdict: ${dir} has other .stringsdict tables (${others.join(", ")}); only ${TABLE2} is imported`
|
|
5549
|
+
);
|
|
5550
|
+
}
|
|
5551
|
+
let root;
|
|
5552
|
+
try {
|
|
5553
|
+
root = parsePlistDict(text);
|
|
5554
|
+
} catch (e) {
|
|
5555
|
+
warnings.push(`apple-stringsdict: failed to parse ${file}: ${e.message}`);
|
|
5556
|
+
continue;
|
|
5557
|
+
}
|
|
5558
|
+
for (const key of Object.keys(root).sort()) {
|
|
5559
|
+
const icu = entryToIcu(key, root[key], file, warnings);
|
|
5560
|
+
if (icu === null) continue;
|
|
5561
|
+
(keys[key] ??= { values: {} }).values[locale] = icu;
|
|
5544
5562
|
}
|
|
5545
5563
|
}
|
|
5546
5564
|
return { locales, keys, warnings };
|
|
@@ -5549,457 +5567,500 @@ var init_i18next_json2 = __esm({
|
|
|
5549
5567
|
}
|
|
5550
5568
|
});
|
|
5551
5569
|
|
|
5552
|
-
// src/server/import/parsers/
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
return
|
|
5557
|
-
}
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
}
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
}
|
|
5596
|
-
}
|
|
5597
|
-
return null;
|
|
5598
|
-
}
|
|
5599
|
-
function stripPlainComment(s) {
|
|
5600
|
-
const m = /(^|\s)#/.exec(s);
|
|
5601
|
-
return (m && m.index >= 0 ? s.slice(0, m.index) : s).trim();
|
|
5602
|
-
}
|
|
5603
|
-
function onlyTrailing(s) {
|
|
5604
|
-
return /^\s*(#.*)?$/.test(s);
|
|
5605
|
-
}
|
|
5606
|
-
function parseYamlSubset(text, file, warnings) {
|
|
5607
|
-
const roots = {};
|
|
5608
|
-
const lines = text.split(/\r?\n/);
|
|
5609
|
-
let stack = [];
|
|
5610
|
-
let skipDeeperThan = null;
|
|
5611
|
-
let lastLeafIndent = null;
|
|
5612
|
-
for (let n = 0; n < lines.length; n++) {
|
|
5613
|
-
const raw = lines[n];
|
|
5614
|
-
const lineNo = n + 1;
|
|
5615
|
-
if (raw.trim() === "" || raw.trim().startsWith("#")) continue;
|
|
5616
|
-
if (raw.trim() === "---") continue;
|
|
5617
|
-
const indentMatch = /^[ \t]*/.exec(raw)[0];
|
|
5618
|
-
if (indentMatch.includes(" ")) {
|
|
5619
|
-
warnings.push(`rails-yaml: ${file}:${lineNo}: tab in indentation; line skipped`);
|
|
5620
|
-
continue;
|
|
5621
|
-
}
|
|
5622
|
-
const indent = indentMatch.length;
|
|
5623
|
-
if (skipDeeperThan !== null) {
|
|
5624
|
-
if (indent > skipDeeperThan) continue;
|
|
5625
|
-
skipDeeperThan = null;
|
|
5626
|
-
}
|
|
5627
|
-
const content = raw.slice(indent);
|
|
5628
|
-
if (content.startsWith("- ") || content === "-") {
|
|
5629
|
-
warnings.push(`rails-yaml: ${file}:${lineNo}: sequences are not supported; node skipped`);
|
|
5630
|
-
skipDeeperThan = indent;
|
|
5631
|
-
continue;
|
|
5632
|
-
}
|
|
5633
|
-
let key;
|
|
5634
|
-
let rest;
|
|
5635
|
-
if (content[0] === '"' || content[0] === "'") {
|
|
5636
|
-
const k = scanQuoted(content, 0);
|
|
5637
|
-
if (!k || content[k.end] !== ":") {
|
|
5638
|
-
warnings.push(`rails-yaml: ${file}:${lineNo}: unparseable quoted key; line skipped`);
|
|
5639
|
-
skipDeeperThan = indent;
|
|
5640
|
-
continue;
|
|
5641
|
-
}
|
|
5642
|
-
key = k.text;
|
|
5643
|
-
rest = content.slice(k.end + 1);
|
|
5644
|
-
} else {
|
|
5645
|
-
const m = /^(.*?):(?=\s|$)/.exec(content);
|
|
5646
|
-
if (!m) {
|
|
5647
|
-
warnings.push(`rails-yaml: ${file}:${lineNo}: not a "key: value" mapping line; line skipped`);
|
|
5648
|
-
skipDeeperThan = indent;
|
|
5649
|
-
continue;
|
|
5650
|
-
}
|
|
5651
|
-
key = m[1].trim();
|
|
5652
|
-
rest = content.slice(m[0].length);
|
|
5653
|
-
}
|
|
5654
|
-
if (lastLeafIndent !== null && indent > lastLeafIndent) {
|
|
5655
|
-
warnings.push(`rails-yaml: ${file}:${lineNo}: unexpected indentation under a scalar; line skipped`);
|
|
5656
|
-
skipDeeperThan = indent - 1;
|
|
5657
|
-
continue;
|
|
5658
|
-
}
|
|
5659
|
-
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) stack.pop();
|
|
5660
|
-
const trimmed = rest.trim();
|
|
5661
|
-
let value;
|
|
5662
|
-
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
5663
|
-
value = null;
|
|
5664
|
-
} else if (trimmed[0] === "&" || trimmed[0] === "*") {
|
|
5665
|
-
warnings.push(`rails-yaml: ${file}:${lineNo}: YAML anchors/aliases are not supported; node skipped`);
|
|
5666
|
-
skipDeeperThan = indent;
|
|
5667
|
-
continue;
|
|
5668
|
-
} else if (trimmed[0] === "|" || trimmed[0] === ">") {
|
|
5669
|
-
warnings.push(`rails-yaml: ${file}:${lineNo}: block scalars are not supported; node skipped`);
|
|
5670
|
-
skipDeeperThan = indent;
|
|
5671
|
-
continue;
|
|
5672
|
-
} else if (trimmed[0] === "[" || trimmed[0] === "{") {
|
|
5673
|
-
warnings.push(`rails-yaml: ${file}:${lineNo}: flow collections are not supported; node skipped`);
|
|
5674
|
-
skipDeeperThan = indent;
|
|
5675
|
-
continue;
|
|
5676
|
-
} else if (trimmed[0] === '"' || trimmed[0] === "'") {
|
|
5677
|
-
const v = scanQuoted(trimmed, 0);
|
|
5678
|
-
if (!v || !onlyTrailing(trimmed.slice(v.end))) {
|
|
5679
|
-
warnings.push(`rails-yaml: ${file}:${lineNo}: unterminated or trailing-garbage quoted value; line skipped`);
|
|
5680
|
-
continue;
|
|
5681
|
-
}
|
|
5682
|
-
value = v.text;
|
|
5683
|
-
} else {
|
|
5684
|
-
value = stripPlainComment(trimmed);
|
|
5685
|
-
}
|
|
5686
|
-
if (stack.length === 0) {
|
|
5687
|
-
if (value !== null) {
|
|
5688
|
-
warnings.push(`rails-yaml: ${file}:${lineNo}: top-level key "${key}" has a scalar value; skipped`);
|
|
5689
|
-
lastLeafIndent = indent;
|
|
5690
|
-
continue;
|
|
5691
|
-
}
|
|
5692
|
-
const root = roots[key] ??= makeNode();
|
|
5693
|
-
stack = [{ indent, node: root }];
|
|
5694
|
-
lastLeafIndent = null;
|
|
5695
|
-
continue;
|
|
5696
|
-
}
|
|
5697
|
-
const parent = stack[stack.length - 1].node;
|
|
5698
|
-
if (key in parent) {
|
|
5699
|
-
warnings.push(`rails-yaml: ${file}:${lineNo}: duplicate key "${key}"; later value wins`);
|
|
5700
|
-
}
|
|
5701
|
-
if (value === null) {
|
|
5702
|
-
const child = makeNode();
|
|
5703
|
-
parent[key] = child;
|
|
5704
|
-
stack.push({ indent, node: child });
|
|
5705
|
-
lastLeafIndent = null;
|
|
5706
|
-
} else {
|
|
5707
|
-
parent[key] = value;
|
|
5708
|
-
lastLeafIndent = indent;
|
|
5570
|
+
// src/server/import/parsers/index.ts
|
|
5571
|
+
function getParser(name) {
|
|
5572
|
+
const p = REGISTRY[name];
|
|
5573
|
+
if (!p) throw new Error(`Unknown format: ${name} (known: ${Object.keys(REGISTRY).join(", ")})`);
|
|
5574
|
+
return p;
|
|
5575
|
+
}
|
|
5576
|
+
var REGISTRY;
|
|
5577
|
+
var init_parsers = __esm({
|
|
5578
|
+
"src/server/import/parsers/index.ts"() {
|
|
5579
|
+
"use strict";
|
|
5580
|
+
init_vue_i18n_json2();
|
|
5581
|
+
init_laravel_php2();
|
|
5582
|
+
init_flutter_arb2();
|
|
5583
|
+
init_apple_strings2();
|
|
5584
|
+
init_angular_xliff2();
|
|
5585
|
+
init_gettext_po2();
|
|
5586
|
+
init_i18next_json2();
|
|
5587
|
+
init_rails_yaml2();
|
|
5588
|
+
init_apple_stringsdict2();
|
|
5589
|
+
REGISTRY = {
|
|
5590
|
+
[vueI18nJson2.name]: vueI18nJson2,
|
|
5591
|
+
[laravelPhp2.name]: laravelPhp2,
|
|
5592
|
+
[flutterArb2.name]: flutterArb2,
|
|
5593
|
+
[appleStrings2.name]: appleStrings2,
|
|
5594
|
+
[angularXliff2.name]: angularXliff2,
|
|
5595
|
+
[gettextPo2.name]: gettextPo2,
|
|
5596
|
+
[i18nextJson2.name]: i18nextJson2,
|
|
5597
|
+
[railsYaml2.name]: railsYaml2,
|
|
5598
|
+
[appleStringsdict2.name]: appleStringsdict2
|
|
5599
|
+
};
|
|
5600
|
+
}
|
|
5601
|
+
});
|
|
5602
|
+
|
|
5603
|
+
// src/server/import/usage.ts
|
|
5604
|
+
function isLocationScannedState(state) {
|
|
5605
|
+
return state.config.outputs.some((o) => LOCATION_SCANNED_ADAPTERS.has(o.adapter));
|
|
5606
|
+
}
|
|
5607
|
+
function buildLocationUsageCache(parsed) {
|
|
5608
|
+
const files = {};
|
|
5609
|
+
for (const [key, pk] of Object.entries(parsed.keys)) {
|
|
5610
|
+
for (const loc of pk.locations ?? []) {
|
|
5611
|
+
const file = files[loc.file] ??= { mtime: 0, size: 0, refs: [], prefixes: [] };
|
|
5612
|
+
file.refs.push({ key, line: loc.line, col: 1, scanner: "angular-xliff" });
|
|
5709
5613
|
}
|
|
5710
5614
|
}
|
|
5711
|
-
return {
|
|
5615
|
+
return { version: CACHE_VERSION, scannedAt: (/* @__PURE__ */ new Date()).toISOString(), files };
|
|
5712
5616
|
}
|
|
5713
|
-
function
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5617
|
+
function usageCounts(cache2) {
|
|
5618
|
+
return {
|
|
5619
|
+
files: Object.keys(cache2.files).length,
|
|
5620
|
+
refs: Object.values(cache2.files).reduce((n, f) => n + f.refs.length, 0)
|
|
5621
|
+
};
|
|
5622
|
+
}
|
|
5623
|
+
function refreshLocationUsage(projectRoot, format) {
|
|
5624
|
+
const det = detect(projectRoot, format);
|
|
5625
|
+
if (!det) return null;
|
|
5626
|
+
const parsed = getParser(det.format).parse(det.localeRoot, { locales: [det.sourceLocale] });
|
|
5627
|
+
const cache2 = buildLocationUsageCache(parsed);
|
|
5628
|
+
saveUsageCache(projectRoot, cache2);
|
|
5629
|
+
return cache2;
|
|
5630
|
+
}
|
|
5631
|
+
var LOCATION_SCANNED_ADAPTERS;
|
|
5632
|
+
var init_usage = __esm({
|
|
5633
|
+
"src/server/import/usage.ts"() {
|
|
5634
|
+
"use strict";
|
|
5635
|
+
init_detect();
|
|
5636
|
+
init_parsers();
|
|
5637
|
+
init_scanner();
|
|
5638
|
+
init_scan();
|
|
5639
|
+
LOCATION_SCANNED_ADAPTERS = /* @__PURE__ */ new Set(["angular-xliff"]);
|
|
5720
5640
|
}
|
|
5721
|
-
|
|
5722
|
-
|
|
5641
|
+
});
|
|
5642
|
+
|
|
5643
|
+
// src/server/spell.ts
|
|
5644
|
+
function spellTokens(value) {
|
|
5645
|
+
return value.replace(ICU_BLOCK, " ").replace(MASK, " ").match(WORD) ?? [];
|
|
5723
5646
|
}
|
|
5724
|
-
function
|
|
5725
|
-
const
|
|
5726
|
-
|
|
5727
|
-
const
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5647
|
+
function ignoreWordsFor(glossary, customWords = []) {
|
|
5648
|
+
const set = /* @__PURE__ */ new Set();
|
|
5649
|
+
const add = (text) => {
|
|
5650
|
+
for (const w of text.match(WORD) ?? []) set.add(w.toLowerCase());
|
|
5651
|
+
};
|
|
5652
|
+
for (const e of glossary) {
|
|
5653
|
+
add(e.term);
|
|
5654
|
+
for (const t of Object.values(e.translations ?? {})) add(t);
|
|
5655
|
+
}
|
|
5656
|
+
for (const w of customWords) add(w);
|
|
5657
|
+
return set;
|
|
5658
|
+
}
|
|
5659
|
+
async function getSpeller(dictId) {
|
|
5660
|
+
const key = norm(dictId);
|
|
5661
|
+
const existing = instances.get(key);
|
|
5662
|
+
if (existing) return existing;
|
|
5663
|
+
if (unavailable.has(key)) return null;
|
|
5664
|
+
try {
|
|
5665
|
+
const nspellMod = await import("nspell");
|
|
5666
|
+
const nspell = nspellMod.default ?? nspellMod;
|
|
5667
|
+
const dictMod = await import(`dictionary-${key}`);
|
|
5668
|
+
const dictExport = dictMod.default ?? dictMod;
|
|
5669
|
+
const dict = typeof dictExport === "function" ? await dictExport() : dictExport;
|
|
5670
|
+
const speller = nspell(dict);
|
|
5671
|
+
instances.set(key, speller);
|
|
5672
|
+
return speller;
|
|
5673
|
+
} catch {
|
|
5674
|
+
unavailable.add(key);
|
|
5675
|
+
return null;
|
|
5676
|
+
} finally {
|
|
5677
|
+
loading.delete(key);
|
|
5678
|
+
}
|
|
5679
|
+
}
|
|
5680
|
+
function spellValue(dictId, value, ignore) {
|
|
5681
|
+
const key = norm(dictId);
|
|
5682
|
+
if (unavailable.has(key)) return [];
|
|
5683
|
+
const spell = instances.get(key);
|
|
5684
|
+
if (!spell) {
|
|
5685
|
+
if (!loading.has(key)) {
|
|
5686
|
+
loading.add(key);
|
|
5687
|
+
void getSpeller(key);
|
|
5733
5688
|
}
|
|
5734
|
-
|
|
5689
|
+
return null;
|
|
5735
5690
|
}
|
|
5736
|
-
|
|
5691
|
+
const cacheKey = key + " " + value;
|
|
5692
|
+
let allBad = cache.get(cacheKey);
|
|
5693
|
+
if (!allBad) {
|
|
5694
|
+
allBad = spellTokens(value).filter((w) => !spell.correct(w));
|
|
5695
|
+
cache.set(cacheKey, allBad);
|
|
5696
|
+
}
|
|
5697
|
+
return allBad.filter((w) => !ignore.has(w.toLowerCase()));
|
|
5737
5698
|
}
|
|
5738
|
-
var
|
|
5739
|
-
var
|
|
5740
|
-
"src/server/
|
|
5699
|
+
var instances, loading, unavailable, cache, norm, ICU_BLOCK, MASK, WORD;
|
|
5700
|
+
var init_spell = __esm({
|
|
5701
|
+
"src/server/spell.ts"() {
|
|
5741
5702
|
"use strict";
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5703
|
+
instances = /* @__PURE__ */ new Map();
|
|
5704
|
+
loading = /* @__PURE__ */ new Set();
|
|
5705
|
+
unavailable = /* @__PURE__ */ new Set();
|
|
5706
|
+
cache = /* @__PURE__ */ new Map();
|
|
5707
|
+
norm = (dictId) => dictId.toLowerCase();
|
|
5708
|
+
ICU_BLOCK = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g;
|
|
5709
|
+
MASK = /\{[^}]*\}|<[^>]*>|:\w+|%[sd]/g;
|
|
5710
|
+
WORD = new RegExp("\\p{L}[\\p{L}''\u2019-]*", "gu");
|
|
5711
|
+
}
|
|
5712
|
+
});
|
|
5713
|
+
|
|
5714
|
+
// src/server/lint/spelling.ts
|
|
5715
|
+
var spellingRule, defaultLoader;
|
|
5716
|
+
var init_spelling = __esm({
|
|
5717
|
+
"src/server/lint/spelling.ts"() {
|
|
5718
|
+
"use strict";
|
|
5719
|
+
init_spell();
|
|
5720
|
+
spellingRule = {
|
|
5721
|
+
id: "spelling",
|
|
5722
|
+
run(state, ctx) {
|
|
5723
|
+
const out = [];
|
|
5724
|
+
for (const key of Object.keys(state.keys)) {
|
|
5725
|
+
const entry = state.keys[key];
|
|
5726
|
+
for (const locale of ctx.targetLocales) {
|
|
5727
|
+
const speller = ctx.spellers.get(locale);
|
|
5728
|
+
if (!speller) continue;
|
|
5729
|
+
const value = entry.values[locale]?.value;
|
|
5730
|
+
if (!value) continue;
|
|
5731
|
+
for (const word of spellTokens(value)) {
|
|
5732
|
+
if (ctx.allowWords.has(word.toLowerCase())) continue;
|
|
5733
|
+
if (!speller.correct(word)) {
|
|
5734
|
+
out.push({ ruleId: "spelling", key, locale, message: `possible misspelling: "${word}"` });
|
|
5735
|
+
}
|
|
5736
|
+
}
|
|
5737
|
+
}
|
|
5738
|
+
}
|
|
5739
|
+
return out;
|
|
5740
|
+
}
|
|
5741
|
+
};
|
|
5742
|
+
defaultLoader = (dictId) => getSpeller(dictId);
|
|
5743
|
+
}
|
|
5744
|
+
});
|
|
5745
|
+
|
|
5746
|
+
// src/server/lint/rules.ts
|
|
5747
|
+
var emptySourceRule, emptyTranslationRule, identicalToSourceRule, whitespaceRule, placeholderMismatchRule, icuMismatchRule, maxLengthRule, glossaryViolationRule, ALL_RULES;
|
|
5748
|
+
var init_rules = __esm({
|
|
5749
|
+
"src/server/lint/rules.ts"() {
|
|
5750
|
+
"use strict";
|
|
5751
|
+
init_scan();
|
|
5752
|
+
init_placeholders();
|
|
5753
|
+
init_glossary();
|
|
5754
|
+
init_spelling();
|
|
5755
|
+
emptySourceRule = {
|
|
5756
|
+
id: "empty-source",
|
|
5757
|
+
run(state, ctx) {
|
|
5758
|
+
const out = [];
|
|
5759
|
+
for (const key of Object.keys(state.keys)) {
|
|
5760
|
+
const entry = state.keys[key];
|
|
5761
|
+
const v = entry.plural ? entry.values[ctx.sourceLocale]?.forms?.other : entry.values[ctx.sourceLocale]?.value;
|
|
5762
|
+
if (!v || !v.trim()) out.push({ ruleId: "empty-source", key, locale: "", message: "source value is empty" });
|
|
5763
|
+
}
|
|
5764
|
+
return out;
|
|
5765
|
+
}
|
|
5766
|
+
};
|
|
5767
|
+
emptyTranslationRule = {
|
|
5768
|
+
id: "empty-translation",
|
|
5769
|
+
// findMissing is the shared "untranslated" walk (also behind the editor's
|
|
5770
|
+
// untranslated check and /scan/missing); a whitespace-only value counts as
|
|
5771
|
+
// missing there, so no separate whitespace pass is needed.
|
|
5772
|
+
run(state) {
|
|
5773
|
+
const out = [];
|
|
5774
|
+
for (const m of findMissing(state)) {
|
|
5775
|
+
out.push({ ruleId: "empty-translation", key: m.key, locale: m.locale, message: "translation is empty or missing" });
|
|
5776
|
+
}
|
|
5777
|
+
return out;
|
|
5778
|
+
}
|
|
5779
|
+
};
|
|
5780
|
+
identicalToSourceRule = {
|
|
5781
|
+
id: "identical-to-source",
|
|
5782
|
+
run(state, ctx) {
|
|
5783
|
+
const out = [];
|
|
5784
|
+
for (const key of Object.keys(state.keys)) {
|
|
5785
|
+
const entry = state.keys[key];
|
|
5786
|
+
if (entry.skipTranslate) continue;
|
|
5787
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
5788
|
+
if (!src) continue;
|
|
5789
|
+
for (const locale of ctx.targetLocales) {
|
|
5790
|
+
const v = entry.values[locale]?.value;
|
|
5791
|
+
if (v && v === src) out.push({ ruleId: "identical-to-source", key, locale, message: "translation is identical to the source" });
|
|
5792
|
+
}
|
|
5793
|
+
}
|
|
5794
|
+
return out;
|
|
5795
|
+
}
|
|
5796
|
+
};
|
|
5797
|
+
whitespaceRule = {
|
|
5798
|
+
id: "whitespace",
|
|
5799
|
+
run(state, ctx) {
|
|
5800
|
+
const out = [];
|
|
5801
|
+
for (const key of Object.keys(state.keys)) {
|
|
5802
|
+
const entry = state.keys[key];
|
|
5803
|
+
const src = entry.values[ctx.sourceLocale]?.value ?? "";
|
|
5804
|
+
const srcEdge = src !== src.trim();
|
|
5805
|
+
for (const locale of ctx.targetLocales) {
|
|
5806
|
+
const v = entry.values[locale]?.value;
|
|
5807
|
+
if (!v) continue;
|
|
5808
|
+
if (v !== v.trim() !== srcEdge) {
|
|
5809
|
+
out.push({ ruleId: "whitespace", key, locale, message: "leading/trailing whitespace differs from the source" });
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5812
|
+
}
|
|
5813
|
+
return out;
|
|
5814
|
+
}
|
|
5815
|
+
};
|
|
5816
|
+
placeholderMismatchRule = {
|
|
5817
|
+
id: "placeholder-mismatch",
|
|
5818
|
+
run(state, ctx) {
|
|
5819
|
+
const out = [];
|
|
5820
|
+
for (const key of Object.keys(state.keys)) {
|
|
5821
|
+
const entry = state.keys[key];
|
|
5822
|
+
if (entry.plural) {
|
|
5823
|
+
const srcForm = entry.values[ctx.sourceLocale]?.forms?.other;
|
|
5824
|
+
if (!srcForm) continue;
|
|
5825
|
+
for (const locale of ctx.targetLocales) {
|
|
5826
|
+
const forms = entry.values[locale]?.forms;
|
|
5827
|
+
if (!forms) continue;
|
|
5828
|
+
const bad = Object.entries(forms).some(
|
|
5829
|
+
([cat, form]) => form && !pluralFormPlaceholdersMatch(cat, srcForm, form)
|
|
5830
|
+
);
|
|
5831
|
+
if (bad) {
|
|
5832
|
+
out.push({ ruleId: "placeholder-mismatch", key, locale, message: "placeholders differ from the source" });
|
|
5833
|
+
}
|
|
5761
5834
|
}
|
|
5762
|
-
const forms = asPluralForms(v);
|
|
5763
|
-
if (forms) addValue(key, locale, synthesizeIcu(forms, file, key, warnings));
|
|
5764
|
-
else flatten(v, key, locale, file);
|
|
5765
|
-
}
|
|
5766
|
-
};
|
|
5767
|
-
for (const file of readdirSync12(localeRoot).sort()) {
|
|
5768
|
-
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
5769
|
-
let text;
|
|
5770
|
-
try {
|
|
5771
|
-
text = readFileSync20(join14(localeRoot, file), "utf8");
|
|
5772
|
-
} catch (e) {
|
|
5773
|
-
warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
|
|
5774
5835
|
continue;
|
|
5775
5836
|
}
|
|
5776
|
-
const
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5837
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
5838
|
+
if (!src) continue;
|
|
5839
|
+
for (const locale of ctx.targetLocales) {
|
|
5840
|
+
const v = entry.values[locale]?.value;
|
|
5841
|
+
if (!v) continue;
|
|
5842
|
+
if (!placeholdersMatch(src, v)) {
|
|
5843
|
+
out.push({ ruleId: "placeholder-mismatch", key, locale, message: "placeholders differ from the source" });
|
|
5781
5844
|
}
|
|
5782
|
-
if (wanted && !wanted.includes(token.toLowerCase())) continue;
|
|
5783
|
-
if (!locales.includes(token)) locales.push(token);
|
|
5784
|
-
flatten(roots[token], "", token, file);
|
|
5785
5845
|
}
|
|
5786
5846
|
}
|
|
5787
|
-
return
|
|
5847
|
+
return out;
|
|
5848
|
+
}
|
|
5849
|
+
};
|
|
5850
|
+
icuMismatchRule = {
|
|
5851
|
+
id: "icu-mismatch",
|
|
5852
|
+
run(state, ctx) {
|
|
5853
|
+
const out = [];
|
|
5854
|
+
for (const key of Object.keys(state.keys)) {
|
|
5855
|
+
const entry = state.keys[key];
|
|
5856
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
5857
|
+
if (!src) continue;
|
|
5858
|
+
const srcIcu = isIcuPluralOrSelect(src);
|
|
5859
|
+
for (const locale of ctx.targetLocales) {
|
|
5860
|
+
const v = entry.values[locale]?.value;
|
|
5861
|
+
if (!v) continue;
|
|
5862
|
+
if (isIcuPluralOrSelect(v) !== srcIcu) {
|
|
5863
|
+
out.push({
|
|
5864
|
+
ruleId: "icu-mismatch",
|
|
5865
|
+
key,
|
|
5866
|
+
locale,
|
|
5867
|
+
message: srcIcu ? "source is an ICU plural/select but the translation is not" : "translation is an ICU plural/select but the source is not"
|
|
5868
|
+
});
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5871
|
+
}
|
|
5872
|
+
return out;
|
|
5873
|
+
}
|
|
5874
|
+
};
|
|
5875
|
+
maxLengthRule = {
|
|
5876
|
+
id: "max-length",
|
|
5877
|
+
run(state, ctx) {
|
|
5878
|
+
const out = [];
|
|
5879
|
+
for (const key of Object.keys(state.keys)) {
|
|
5880
|
+
const entry = state.keys[key];
|
|
5881
|
+
const max = entry.maxLength;
|
|
5882
|
+
if (max == null) continue;
|
|
5883
|
+
for (const locale of ctx.targetLocales) {
|
|
5884
|
+
const v = entry.values[locale]?.value;
|
|
5885
|
+
if (v && v.length > max) {
|
|
5886
|
+
out.push({ ruleId: "max-length", key, locale, message: `length ${v.length} exceeds maxLength ${max}` });
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
}
|
|
5890
|
+
return out;
|
|
5891
|
+
}
|
|
5892
|
+
};
|
|
5893
|
+
glossaryViolationRule = {
|
|
5894
|
+
id: "glossary-violation",
|
|
5895
|
+
run(state, ctx) {
|
|
5896
|
+
const out = [];
|
|
5897
|
+
for (const key of Object.keys(state.keys)) {
|
|
5898
|
+
const entry = state.keys[key];
|
|
5899
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
5900
|
+
if (!src) continue;
|
|
5901
|
+
for (const locale of ctx.targetLocales) {
|
|
5902
|
+
const v = entry.values[locale]?.value;
|
|
5903
|
+
if (!v) continue;
|
|
5904
|
+
for (const viol of glossaryViolations(src, v, locale, ctx.glossary)) {
|
|
5905
|
+
out.push({
|
|
5906
|
+
ruleId: "glossary-violation",
|
|
5907
|
+
key,
|
|
5908
|
+
locale,
|
|
5909
|
+
message: viol.kind === "do-not-translate" ? `do-not-translate term "${viol.term}" is missing or altered` : `expected glossary translation "${viol.expected}" for "${viol.term}"`
|
|
5910
|
+
});
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
return out;
|
|
5788
5915
|
}
|
|
5789
5916
|
};
|
|
5917
|
+
ALL_RULES = [
|
|
5918
|
+
emptySourceRule,
|
|
5919
|
+
emptyTranslationRule,
|
|
5920
|
+
placeholderMismatchRule,
|
|
5921
|
+
icuMismatchRule,
|
|
5922
|
+
glossaryViolationRule,
|
|
5923
|
+
maxLengthRule,
|
|
5924
|
+
identicalToSourceRule,
|
|
5925
|
+
whitespaceRule,
|
|
5926
|
+
spellingRule
|
|
5927
|
+
];
|
|
5790
5928
|
}
|
|
5791
5929
|
});
|
|
5792
5930
|
|
|
5793
|
-
// src/server/
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
function localeFromLproj2(dir) {
|
|
5797
|
-
const m = dir.match(/^(.+)\.lproj$/);
|
|
5798
|
-
if (!m) return null;
|
|
5799
|
-
return LOCALE_RE9.test(m[1]) ? m[1] : null;
|
|
5931
|
+
// src/server/lint/run.ts
|
|
5932
|
+
function resolveSeverity(id, config) {
|
|
5933
|
+
return config.rules?.[id] ?? DEFAULT_SEVERITY[id];
|
|
5800
5934
|
}
|
|
5801
|
-
function
|
|
5802
|
-
return
|
|
5935
|
+
function sortFindings(findings) {
|
|
5936
|
+
return [...findings].sort(
|
|
5937
|
+
(a, b) => a.key.localeCompare(b.key) || a.locale.localeCompare(b.locale) || a.ruleId.localeCompare(b.ruleId)
|
|
5938
|
+
);
|
|
5803
5939
|
}
|
|
5804
|
-
function
|
|
5805
|
-
let
|
|
5806
|
-
const
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
const
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
const closing = body.startsWith("/");
|
|
5832
|
-
if (closing) body = body.slice(1).trim();
|
|
5833
|
-
const selfClosing = body.endsWith("/");
|
|
5834
|
-
if (selfClosing) body = body.slice(0, -1).trim();
|
|
5835
|
-
const name = body.split(/\s/)[0];
|
|
5836
|
-
if (!name) throw new Error(`empty tag at offset ${end}`);
|
|
5837
|
-
return { name, closing, selfClosing };
|
|
5838
|
-
};
|
|
5839
|
-
const readElementText = (name) => {
|
|
5840
|
-
const re = new RegExp(`</${name}\\s*>`, "g");
|
|
5841
|
-
re.lastIndex = i;
|
|
5842
|
-
const m = re.exec(xml);
|
|
5843
|
-
if (!m) throw new Error(`unterminated <${name}>`);
|
|
5844
|
-
const text = xml.slice(i, m.index);
|
|
5845
|
-
i = m.index + m[0].length;
|
|
5846
|
-
return decodeEntities2(text);
|
|
5940
|
+
function countSeverities(findings) {
|
|
5941
|
+
let error = 0, warn = 0;
|
|
5942
|
+
for (const f of findings) {
|
|
5943
|
+
if (f.suppressed) continue;
|
|
5944
|
+
f.severity === "error" ? error++ : warn++;
|
|
5945
|
+
}
|
|
5946
|
+
return { error, warn };
|
|
5947
|
+
}
|
|
5948
|
+
async function loadSpellers(locales, config, load, warn) {
|
|
5949
|
+
const map = /* @__PURE__ */ new Map();
|
|
5950
|
+
for (const locale of locales) {
|
|
5951
|
+
const dictId = config.spelling?.locales?.[locale] ?? locale;
|
|
5952
|
+
const speller = await load(dictId);
|
|
5953
|
+
if (speller) map.set(locale, speller);
|
|
5954
|
+
else warn(`no dictionary for "${locale}", skipping spelling`);
|
|
5955
|
+
}
|
|
5956
|
+
return map;
|
|
5957
|
+
}
|
|
5958
|
+
async function runLint(state, options = {}) {
|
|
5959
|
+
const config = state.config.lint ?? {};
|
|
5960
|
+
const rules = options.rules ?? ALL_RULES;
|
|
5961
|
+
const warn = options.warn ?? ((m) => console.warn(m));
|
|
5962
|
+
const load = options.loadSpeller ?? defaultLoader;
|
|
5963
|
+
const targetLocales = state.config.locales.filter((l) => l !== state.config.sourceLocale);
|
|
5964
|
+
const isActive = (rule) => {
|
|
5965
|
+
if (options.ruleIds && !options.ruleIds.includes(rule.id)) return false;
|
|
5966
|
+
return resolveSeverity(rule.id, config) !== "off";
|
|
5847
5967
|
};
|
|
5848
|
-
const
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5968
|
+
const active = rules.filter(isActive);
|
|
5969
|
+
const spellingOn = active.some((r) => r.id === "spelling");
|
|
5970
|
+
const spellers = spellingOn ? await loadSpellers(targetLocales, config, load, warn) : /* @__PURE__ */ new Map();
|
|
5971
|
+
const allowWords = spellingOn ? ignoreWordsFor(state.glossary, state.config.spelling?.customWords) : /* @__PURE__ */ new Set();
|
|
5972
|
+
const ctx = {
|
|
5973
|
+
config,
|
|
5974
|
+
sourceLocale: state.config.sourceLocale,
|
|
5975
|
+
targetLocales,
|
|
5976
|
+
glossary: state.glossary,
|
|
5977
|
+
spellers,
|
|
5978
|
+
allowWords
|
|
5858
5979
|
};
|
|
5859
|
-
const
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5980
|
+
const ignoreRes = (config.ignore ?? []).map(globToRegExp);
|
|
5981
|
+
const localeFilter = options.locales ? new Set(options.locales) : null;
|
|
5982
|
+
const findings = [];
|
|
5983
|
+
let suppressed = 0;
|
|
5984
|
+
for (const rule of active) {
|
|
5985
|
+
const severity = resolveSeverity(rule.id, config);
|
|
5986
|
+
for (const raw of rule.run(state, ctx)) {
|
|
5987
|
+
if (ignoreRes.some((re) => re.test(raw.key))) continue;
|
|
5988
|
+
if (localeFilter && raw.locale !== "" && !localeFilter.has(raw.locale)) continue;
|
|
5989
|
+
const entry = state.keys[raw.key];
|
|
5990
|
+
if (raw.locale !== "" && entry && findSuppression(entry, state.config.sourceLocale, rule.id, raw.locale)) {
|
|
5991
|
+
suppressed++;
|
|
5992
|
+
if (options.includeSuppressed) findings.push({ ...raw, severity, suppressed: true });
|
|
5993
|
+
continue;
|
|
5994
|
+
}
|
|
5995
|
+
findings.push({ ...raw, severity });
|
|
5874
5996
|
}
|
|
5875
|
-
};
|
|
5876
|
-
skipTrivia();
|
|
5877
|
-
let tag = readTag();
|
|
5878
|
-
if (tag.name === "plist" && !tag.closing && !tag.selfClosing) {
|
|
5879
|
-
skipTrivia();
|
|
5880
|
-
tag = readTag();
|
|
5881
5997
|
}
|
|
5882
|
-
|
|
5883
|
-
|
|
5998
|
+
const sorted = sortFindings(findings);
|
|
5999
|
+
const counts = { ...countSeverities(sorted), suppressed };
|
|
6000
|
+
return { findings: sorted, counts, ok: counts.error === 0 };
|
|
5884
6001
|
}
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
if (vars.length !== 1) {
|
|
5895
|
-
return warn(`format key has ${vars.length} %#@\u2026@ variables; only exactly one is supported; skipped`);
|
|
5896
|
-
}
|
|
5897
|
-
const arg = vars[0][1];
|
|
5898
|
-
if (!/^\w+$/.test(arg)) return warn(`variable name "${arg}" is not a valid ICU argument; skipped`);
|
|
5899
|
-
const prefix = fmt.slice(0, vars[0].index);
|
|
5900
|
-
const suffix = fmt.slice(vars[0].index + vars[0][0].length);
|
|
5901
|
-
const varDict = entry[arg];
|
|
5902
|
-
if (typeof varDict !== "object") return warn(`variable "${arg}" has no dict; skipped`);
|
|
5903
|
-
const specType = varDict["NSStringFormatSpecTypeKey"];
|
|
5904
|
-
if (specType !== void 0 && specType !== "NSStringPluralRuleType") {
|
|
5905
|
-
return warn(`variable "${arg}" is not a plural rule (${String(specType)}); skipped`);
|
|
6002
|
+
var init_run2 = __esm({
|
|
6003
|
+
"src/server/lint/run.ts"() {
|
|
6004
|
+
"use strict";
|
|
6005
|
+
init_glob();
|
|
6006
|
+
init_registry();
|
|
6007
|
+
init_rules();
|
|
6008
|
+
init_spelling();
|
|
6009
|
+
init_spell();
|
|
6010
|
+
init_suppress();
|
|
5906
6011
|
}
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
6012
|
+
});
|
|
6013
|
+
|
|
6014
|
+
// src/server/lint/outputs.ts
|
|
6015
|
+
import { readFileSync as readFileSync21, existsSync as existsSync12 } from "fs";
|
|
6016
|
+
import { resolve as resolve8 } from "path";
|
|
6017
|
+
function checkOutputs(state, root) {
|
|
6018
|
+
const out = [];
|
|
6019
|
+
for (const output of state.config.outputs) {
|
|
6020
|
+
const result = getAdapter(output.adapter).export(state, output);
|
|
6021
|
+
for (const file of result.files) {
|
|
6022
|
+
const abs = resolve8(root, file.path);
|
|
6023
|
+
const current = existsSync12(abs) ? readFileSync21(abs, "utf8") : null;
|
|
6024
|
+
if (current === null) {
|
|
6025
|
+
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
6026
|
+
} else if (current !== file.contents) {
|
|
6027
|
+
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is out of date; run `glotfile export`" });
|
|
6028
|
+
}
|
|
6029
|
+
}
|
|
5914
6030
|
}
|
|
5915
|
-
|
|
5916
|
-
return formsToIcu(arg, forms);
|
|
6031
|
+
return out;
|
|
5917
6032
|
}
|
|
5918
|
-
var
|
|
5919
|
-
|
|
5920
|
-
"src/server/import/parsers/apple-stringsdict.ts"() {
|
|
6033
|
+
var init_outputs = __esm({
|
|
6034
|
+
"src/server/lint/outputs.ts"() {
|
|
5921
6035
|
"use strict";
|
|
5922
|
-
|
|
5923
|
-
init_plurals();
|
|
5924
|
-
LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5925
|
-
TABLE2 = "Localizable.stringsdict";
|
|
5926
|
-
VAR_RE = /%#@([^@]*)@/g;
|
|
5927
|
-
appleStringsdict2 = {
|
|
5928
|
-
name: "apple-stringsdict",
|
|
5929
|
-
parse(localeRoot, opts) {
|
|
5930
|
-
const warnings = [];
|
|
5931
|
-
const keys = {};
|
|
5932
|
-
const locales = [];
|
|
5933
|
-
for (const dir of readdirSync13(localeRoot).sort()) {
|
|
5934
|
-
const locale = localeFromLproj2(dir);
|
|
5935
|
-
if (!locale) continue;
|
|
5936
|
-
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5937
|
-
const file = join15(localeRoot, dir, TABLE2);
|
|
5938
|
-
let text;
|
|
5939
|
-
try {
|
|
5940
|
-
if (!statSync7(file).isFile()) continue;
|
|
5941
|
-
text = readFileSync21(file, "utf8");
|
|
5942
|
-
} catch {
|
|
5943
|
-
continue;
|
|
5944
|
-
}
|
|
5945
|
-
locales.push(locale);
|
|
5946
|
-
const others = readdirSync13(join15(localeRoot, dir)).filter(
|
|
5947
|
-
(f) => f.endsWith(".stringsdict") && f !== TABLE2
|
|
5948
|
-
);
|
|
5949
|
-
if (others.length) {
|
|
5950
|
-
warnings.push(
|
|
5951
|
-
`apple-stringsdict: ${dir} has other .stringsdict tables (${others.join(", ")}); only ${TABLE2} is imported`
|
|
5952
|
-
);
|
|
5953
|
-
}
|
|
5954
|
-
let root;
|
|
5955
|
-
try {
|
|
5956
|
-
root = parsePlistDict(text);
|
|
5957
|
-
} catch (e) {
|
|
5958
|
-
warnings.push(`apple-stringsdict: failed to parse ${file}: ${e.message}`);
|
|
5959
|
-
continue;
|
|
5960
|
-
}
|
|
5961
|
-
for (const key of Object.keys(root).sort()) {
|
|
5962
|
-
const icu = entryToIcu(key, root[key], file, warnings);
|
|
5963
|
-
if (icu === null) continue;
|
|
5964
|
-
(keys[key] ??= { values: {} }).values[locale] = icu;
|
|
5965
|
-
}
|
|
5966
|
-
}
|
|
5967
|
-
return { locales, keys, warnings };
|
|
5968
|
-
}
|
|
5969
|
-
};
|
|
6036
|
+
init_adapters();
|
|
5970
6037
|
}
|
|
5971
6038
|
});
|
|
5972
6039
|
|
|
5973
|
-
// src/server/
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
6040
|
+
// src/server/lint/accept.ts
|
|
6041
|
+
var accept_exports = {};
|
|
6042
|
+
__export(accept_exports, {
|
|
6043
|
+
acceptFindings: () => acceptFindings
|
|
6044
|
+
});
|
|
6045
|
+
function acceptFindings(state, findings, opts = {}, clock = systemClock) {
|
|
6046
|
+
const byRule = {};
|
|
6047
|
+
let accepted = 0;
|
|
6048
|
+
for (const f of findings) {
|
|
6049
|
+
if (f.locale === "" || f.suppressed) continue;
|
|
6050
|
+
if (f.severity === "error" && !opts.includeErrors) continue;
|
|
6051
|
+
if (opts.rules && !opts.rules.includes(f.ruleId)) continue;
|
|
6052
|
+
if (opts.locales && !opts.locales.includes(f.locale)) continue;
|
|
6053
|
+
if (!state.keys[f.key]) continue;
|
|
6054
|
+
addSuppression(state, f.key, f.ruleId, f.locale, clock);
|
|
6055
|
+
byRule[f.ruleId] = (byRule[f.ruleId] ?? 0) + 1;
|
|
6056
|
+
accepted++;
|
|
6057
|
+
}
|
|
6058
|
+
return { accepted, byRule };
|
|
5978
6059
|
}
|
|
5979
|
-
var
|
|
5980
|
-
|
|
5981
|
-
"src/server/import/parsers/index.ts"() {
|
|
6060
|
+
var init_accept = __esm({
|
|
6061
|
+
"src/server/lint/accept.ts"() {
|
|
5982
6062
|
"use strict";
|
|
5983
|
-
|
|
5984
|
-
init_laravel_php2();
|
|
5985
|
-
init_flutter_arb2();
|
|
5986
|
-
init_apple_strings2();
|
|
5987
|
-
init_angular_xliff2();
|
|
5988
|
-
init_gettext_po2();
|
|
5989
|
-
init_i18next_json2();
|
|
5990
|
-
init_rails_yaml2();
|
|
5991
|
-
init_apple_stringsdict2();
|
|
5992
|
-
REGISTRY = {
|
|
5993
|
-
[vueI18nJson2.name]: vueI18nJson2,
|
|
5994
|
-
[laravelPhp2.name]: laravelPhp2,
|
|
5995
|
-
[flutterArb2.name]: flutterArb2,
|
|
5996
|
-
[appleStrings2.name]: appleStrings2,
|
|
5997
|
-
[angularXliff2.name]: angularXliff2,
|
|
5998
|
-
[gettextPo2.name]: gettextPo2,
|
|
5999
|
-
[i18nextJson2.name]: i18nextJson2,
|
|
6000
|
-
[railsYaml2.name]: railsYaml2,
|
|
6001
|
-
[appleStringsdict2.name]: appleStringsdict2
|
|
6002
|
-
};
|
|
6063
|
+
init_state();
|
|
6003
6064
|
}
|
|
6004
6065
|
});
|
|
6005
6066
|
|
|
@@ -6096,11 +6157,107 @@ var init_assemble = __esm({
|
|
|
6096
6157
|
}
|
|
6097
6158
|
});
|
|
6098
6159
|
|
|
6160
|
+
// src/server/import/merge.ts
|
|
6161
|
+
function hasContent(lv) {
|
|
6162
|
+
if (!lv) return false;
|
|
6163
|
+
return !!(lv.forms ? lv.forms.other?.trim() : lv.value?.trim());
|
|
6164
|
+
}
|
|
6165
|
+
function formsEqual(a, b) {
|
|
6166
|
+
const ak = Object.keys(a ?? {}).sort();
|
|
6167
|
+
const bk = Object.keys(b ?? {}).sort();
|
|
6168
|
+
if (ak.length !== bk.length || ak.some((k, i) => k !== bk[i])) return false;
|
|
6169
|
+
return ak.every((k) => a[k] === b[k]);
|
|
6170
|
+
}
|
|
6171
|
+
function sameSource(cur, inc, src) {
|
|
6172
|
+
if (!!cur.plural !== !!inc.plural) return false;
|
|
6173
|
+
const c = cur.values[src];
|
|
6174
|
+
const i = inc.values[src];
|
|
6175
|
+
return cur.plural ? formsEqual(c?.forms, i?.forms) : (c?.value ?? "") === (i?.value ?? "");
|
|
6176
|
+
}
|
|
6177
|
+
function applyIncomingSource(cur, inc, src, shapeChanged) {
|
|
6178
|
+
const incSrc = inc.values[src];
|
|
6179
|
+
if (shapeChanged) {
|
|
6180
|
+
if (inc.plural) cur.plural = { arg: inc.plural.arg };
|
|
6181
|
+
else delete cur.plural;
|
|
6182
|
+
cur.values = { [src]: { ...incSrc, state: "source" } };
|
|
6183
|
+
return;
|
|
6184
|
+
}
|
|
6185
|
+
const existing = cur.values[src];
|
|
6186
|
+
if (cur.plural) cur.values[src] = { ...existing, forms: incSrc?.forms, state: "source" };
|
|
6187
|
+
else cur.values[src] = { ...existing, value: incSrc?.value, state: "source" };
|
|
6188
|
+
}
|
|
6189
|
+
function cloneForAdd(inc, allowed) {
|
|
6190
|
+
const entry = structuredClone(inc);
|
|
6191
|
+
for (const loc of Object.keys(entry.values)) {
|
|
6192
|
+
if (!allowed.has(loc)) delete entry.values[loc];
|
|
6193
|
+
}
|
|
6194
|
+
return entry;
|
|
6195
|
+
}
|
|
6196
|
+
function mergeStates(existing, incoming, opts = {}) {
|
|
6197
|
+
const state = structuredClone(existing);
|
|
6198
|
+
const src = state.config.sourceLocale;
|
|
6199
|
+
const targets = state.config.locales.filter((l) => l !== src);
|
|
6200
|
+
const allowed = new Set(state.config.locales);
|
|
6201
|
+
const live = opts.liveKeys ?? new Set(Object.keys(incoming.keys));
|
|
6202
|
+
const plan = { added: [], sourceChanged: [], adopted: [], removed: [], unchanged: 0 };
|
|
6203
|
+
for (const [key, inc] of Object.entries(incoming.keys)) {
|
|
6204
|
+
if (!live.has(key)) continue;
|
|
6205
|
+
const cur = state.keys[key];
|
|
6206
|
+
if (!cur) {
|
|
6207
|
+
const entry = cloneForAdd(inc, allowed);
|
|
6208
|
+
if (!entry.createdAt) entry.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6209
|
+
state.keys[key] = entry;
|
|
6210
|
+
plan.added.push(key);
|
|
6211
|
+
continue;
|
|
6212
|
+
}
|
|
6213
|
+
const shapeChanged = !!cur.plural !== !!inc.plural;
|
|
6214
|
+
const srcChanged = !sameSource(cur, inc, src);
|
|
6215
|
+
if (srcChanged) {
|
|
6216
|
+
applyIncomingSource(cur, inc, src, shapeChanged);
|
|
6217
|
+
plan.sourceChanged.push(key);
|
|
6218
|
+
for (const loc of targets) {
|
|
6219
|
+
const lv = cur.values[loc];
|
|
6220
|
+
if (lv && hasContent(lv)) lv.state = "needs-review";
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
6223
|
+
if (inc.placeholders) cur.placeholders = inc.placeholders;
|
|
6224
|
+
else delete cur.placeholders;
|
|
6225
|
+
if (!cur.description && inc.description) cur.description = inc.description;
|
|
6226
|
+
let adoptedHere = false;
|
|
6227
|
+
for (const loc of targets) {
|
|
6228
|
+
const incLv = inc.values[loc];
|
|
6229
|
+
if (!hasContent(incLv)) continue;
|
|
6230
|
+
if (hasContent(cur.values[loc])) continue;
|
|
6231
|
+
cur.values[loc] = { ...structuredClone(incLv), state: "reviewed" };
|
|
6232
|
+
plan.adopted.push({ key, locale: loc });
|
|
6233
|
+
adoptedHere = true;
|
|
6234
|
+
}
|
|
6235
|
+
if (!srcChanged && !adoptedHere) plan.unchanged++;
|
|
6236
|
+
}
|
|
6237
|
+
for (const key of Object.keys(state.keys)) {
|
|
6238
|
+
if (!live.has(key)) {
|
|
6239
|
+
plan.removed.push(key);
|
|
6240
|
+
if (opts.prune) delete state.keys[key];
|
|
6241
|
+
}
|
|
6242
|
+
}
|
|
6243
|
+
plan.added.sort();
|
|
6244
|
+
plan.sourceChanged.sort();
|
|
6245
|
+
plan.removed.sort();
|
|
6246
|
+
plan.adopted.sort((a, b) => a.key.localeCompare(b.key) || a.locale.localeCompare(b.locale));
|
|
6247
|
+
return { state, plan };
|
|
6248
|
+
}
|
|
6249
|
+
var init_merge = __esm({
|
|
6250
|
+
"src/server/import/merge.ts"() {
|
|
6251
|
+
"use strict";
|
|
6252
|
+
}
|
|
6253
|
+
});
|
|
6254
|
+
|
|
6099
6255
|
// src/server/import/run.ts
|
|
6100
6256
|
var run_exports = {};
|
|
6101
6257
|
__export(run_exports, {
|
|
6102
6258
|
previewImport: () => previewImport,
|
|
6103
|
-
runImport: () => runImport
|
|
6259
|
+
runImport: () => runImport,
|
|
6260
|
+
runSync: () => runSync
|
|
6104
6261
|
});
|
|
6105
6262
|
import { relative as relative3 } from "path";
|
|
6106
6263
|
function previewImport(projectRoot, format) {
|
|
@@ -6125,6 +6282,29 @@ function previewImport(projectRoot, format) {
|
|
|
6125
6282
|
sampleKeys
|
|
6126
6283
|
};
|
|
6127
6284
|
}
|
|
6285
|
+
function runSync(opts) {
|
|
6286
|
+
const det = detect(opts.projectRoot, opts.format);
|
|
6287
|
+
if (!det) throw new Error(`No recognized locale files found in ${opts.projectRoot}`);
|
|
6288
|
+
const parser = getParser(det.format);
|
|
6289
|
+
const sourceLocale = opts.sourceLocale ?? det.sourceLocale;
|
|
6290
|
+
const parsed = parser.parse(
|
|
6291
|
+
det.localeRoot,
|
|
6292
|
+
opts.locales ? { locales: opts.locales } : void 0
|
|
6293
|
+
);
|
|
6294
|
+
const sourceParse = parser.parse(det.localeRoot, { locales: [sourceLocale] });
|
|
6295
|
+
const liveKeys = new Set(Object.keys(sourceParse.keys));
|
|
6296
|
+
const assembled = assemble2(parsed, {
|
|
6297
|
+
sourceLocale,
|
|
6298
|
+
format: det.format,
|
|
6299
|
+
cldr: opts.cldr,
|
|
6300
|
+
localeRootRel: relative3(opts.projectRoot, det.localeRoot)
|
|
6301
|
+
});
|
|
6302
|
+
const { warnings, ...rest } = assembled;
|
|
6303
|
+
const incoming = validate(rest);
|
|
6304
|
+
const existing = loadState(opts.statePath);
|
|
6305
|
+
const { state, plan } = mergeStates(existing, incoming, { prune: opts.prune, liveKeys });
|
|
6306
|
+
return { state, plan, warnings, keyCount: Object.keys(state.keys).length };
|
|
6307
|
+
}
|
|
6128
6308
|
function runImport(opts) {
|
|
6129
6309
|
const det = detect(opts.projectRoot, opts.format);
|
|
6130
6310
|
if (!det) throw new Error(`No recognized locale files found in ${opts.projectRoot}`);
|
|
@@ -6154,6 +6334,8 @@ var init_run3 = __esm({
|
|
|
6154
6334
|
init_detect();
|
|
6155
6335
|
init_parsers();
|
|
6156
6336
|
init_assemble();
|
|
6337
|
+
init_merge();
|
|
6338
|
+
init_state();
|
|
6157
6339
|
init_schema();
|
|
6158
6340
|
}
|
|
6159
6341
|
});
|
|
@@ -7063,6 +7245,39 @@ function createApi(deps) {
|
|
|
7063
7245
|
console.log(`[import] ${result.keyCount} key(s) across ${result.localeCount} locale(s)${result.warnings.length ? `, ${result.warnings.length} warning(s)` : ""}`);
|
|
7064
7246
|
return c.json({ keyCount: result.keyCount, localeCount: result.localeCount, warnings: result.warnings });
|
|
7065
7247
|
});
|
|
7248
|
+
app.post("/sync", async (c) => {
|
|
7249
|
+
if (Object.keys(load().keys).length === 0) {
|
|
7250
|
+
return c.json({ error: "nothing to sync into; import first" }, 400);
|
|
7251
|
+
}
|
|
7252
|
+
const body = await c.req.json().catch(() => ({}));
|
|
7253
|
+
let result;
|
|
7254
|
+
try {
|
|
7255
|
+
result = runSync({
|
|
7256
|
+
projectRoot,
|
|
7257
|
+
statePath: deps.statePath,
|
|
7258
|
+
format: body.format,
|
|
7259
|
+
sourceLocale: body.sourceLocale,
|
|
7260
|
+
locales: body.locales,
|
|
7261
|
+
cldr: body.cldr,
|
|
7262
|
+
prune: body.prune
|
|
7263
|
+
});
|
|
7264
|
+
} catch (e) {
|
|
7265
|
+
return c.json({ error: e.message }, 400);
|
|
7266
|
+
}
|
|
7267
|
+
if (body.apply !== true) {
|
|
7268
|
+
return c.json({ plan: result.plan, warnings: result.warnings });
|
|
7269
|
+
}
|
|
7270
|
+
persist(result.state);
|
|
7271
|
+
const usageCache = isLocationScannedState(result.state) ? refreshLocationUsage(projectRoot, body.format) : null;
|
|
7272
|
+
const usageRefs = usageCache ? usageCounts(usageCache).refs : void 0;
|
|
7273
|
+
const p = result.plan;
|
|
7274
|
+
logChange({
|
|
7275
|
+
kind: "import",
|
|
7276
|
+
summary: `Synced: +${p.added.length} added, ~${p.sourceChanged.length} changed, -${p.removed.length} removed${body.prune ? " (pruned)" : ""}`
|
|
7277
|
+
});
|
|
7278
|
+
console.log(`[sync] +${p.added.length} ~${p.sourceChanged.length} -${p.removed.length}${body.prune ? " pruned" : ""}`);
|
|
7279
|
+
return c.json({ applied: true, plan: result.plan, warnings: result.warnings, usageRefs });
|
|
7280
|
+
});
|
|
7066
7281
|
app.post("/export", (c) => {
|
|
7067
7282
|
const root = dirname3(resolve9(deps.statePath));
|
|
7068
7283
|
const { written, skipped, deleted, warnings } = exportToDisk(load(), root);
|
|
@@ -7326,12 +7541,11 @@ function createApi(deps) {
|
|
|
7326
7541
|
app.get("/log", (c) => c.json(readLog(projectRoot, 100)));
|
|
7327
7542
|
app.post("/scan", async (c) => {
|
|
7328
7543
|
const s = load();
|
|
7329
|
-
const
|
|
7330
|
-
|
|
7331
|
-
const
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
return c.json({ files: fileCount2, refs: refCount, scannedAt: result.scannedAt });
|
|
7544
|
+
const result = isLocationScannedState(s) ? refreshLocationUsage(projectRoot) : runScan(projectRoot, s.config.scan ?? {}, loadUsageCache(projectRoot));
|
|
7545
|
+
if (!result) return c.json({ files: 0, refs: 0, scannedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
7546
|
+
const { files, refs } = usageCounts(result);
|
|
7547
|
+
console.log(`[scan] ${files} file(s), ${refs} reference(s)`);
|
|
7548
|
+
return c.json({ files, refs, scannedAt: result.scannedAt });
|
|
7335
7549
|
});
|
|
7336
7550
|
app.get("/scan", (c) => {
|
|
7337
7551
|
const cache2 = loadUsageCache(projectRoot);
|
|
@@ -7597,6 +7811,7 @@ var init_api = __esm({
|
|
|
7597
7811
|
init_log();
|
|
7598
7812
|
init_schema();
|
|
7599
7813
|
init_run3();
|
|
7814
|
+
init_usage();
|
|
7600
7815
|
init_export_run();
|
|
7601
7816
|
init_ui_prefs();
|
|
7602
7817
|
init_local_settings();
|
|
@@ -7698,11 +7913,16 @@ function backgroundScan(statePath) {
|
|
|
7698
7913
|
const projectRoot = dirname4(resolve10(statePath));
|
|
7699
7914
|
Promise.resolve().then(() => {
|
|
7700
7915
|
const state = loadState(statePath);
|
|
7916
|
+
if (isLocationScannedState(state)) {
|
|
7917
|
+
const cache2 = refreshLocationUsage(projectRoot);
|
|
7918
|
+
const { files: files2, refs: refs2 } = cache2 ? usageCounts(cache2) : { files: 0, refs: 0 };
|
|
7919
|
+
console.log(`[scan] ${files2} file(s), ${refs2} reference(s) (from catalog locations)`);
|
|
7920
|
+
return;
|
|
7921
|
+
}
|
|
7701
7922
|
const existing = loadUsageCache(projectRoot);
|
|
7702
7923
|
const result = runScan(projectRoot, state.config.scan ?? {}, existing);
|
|
7703
|
-
const
|
|
7704
|
-
|
|
7705
|
-
console.log(`[scan] ${fileCount2} file(s), ${refCount} reference(s)`);
|
|
7924
|
+
const { files, refs } = usageCounts(result);
|
|
7925
|
+
console.log(`[scan] ${files} file(s), ${refs} reference(s)`);
|
|
7706
7926
|
}).catch((err) => {
|
|
7707
7927
|
console.warn("[scan] failed:", err instanceof Error ? err.message : String(err));
|
|
7708
7928
|
});
|
|
@@ -7715,6 +7935,7 @@ var init_server = __esm({
|
|
|
7715
7935
|
init_state();
|
|
7716
7936
|
init_scan();
|
|
7717
7937
|
init_scanner();
|
|
7938
|
+
init_usage();
|
|
7718
7939
|
here = dirname4(fileURLToPath(import.meta.url));
|
|
7719
7940
|
DEFAULT_UI_DIR = join17(here, "..", "ui");
|
|
7720
7941
|
MIME = {
|
|
@@ -7762,6 +7983,7 @@ init_pricing();
|
|
|
7762
7983
|
init_log();
|
|
7763
7984
|
init_scan();
|
|
7764
7985
|
init_scanner();
|
|
7986
|
+
init_usage();
|
|
7765
7987
|
init_context();
|
|
7766
7988
|
init_run2();
|
|
7767
7989
|
init_outputs();
|
|
@@ -7833,7 +8055,7 @@ function formatSarif(report, rawText) {
|
|
|
7833
8055
|
}
|
|
7834
8056
|
|
|
7835
8057
|
// src/server/cli.ts
|
|
7836
|
-
var COMMANDS = ["serve", "export", "translate", "lint", "check", "import", "build-context", "scan", "prune", "split", "skill", "batch"];
|
|
8058
|
+
var COMMANDS = ["serve", "export", "translate", "lint", "check", "import", "sync", "build-context", "scan", "prune", "split", "skill", "batch"];
|
|
7837
8059
|
var isCommand = (s) => s != null && COMMANDS.includes(s);
|
|
7838
8060
|
function parseArgs(argv) {
|
|
7839
8061
|
const statePath = resolve11(process.cwd(), "glotfile.json");
|
|
@@ -7873,7 +8095,7 @@ function parseArgs(argv) {
|
|
|
7873
8095
|
i++;
|
|
7874
8096
|
} else if (flag === "--watch") args.watch = true;
|
|
7875
8097
|
else if (flag === "--format" && next) {
|
|
7876
|
-
if (args.command === "import") args.importFormat = next;
|
|
8098
|
+
if (args.command === "import" || args.command === "sync") args.importFormat = next;
|
|
7877
8099
|
else args.format = next;
|
|
7878
8100
|
i++;
|
|
7879
8101
|
} else if (flag === "--source" && next) {
|
|
@@ -7884,6 +8106,8 @@ function parseArgs(argv) {
|
|
|
7884
8106
|
i++;
|
|
7885
8107
|
} else if (flag === "--force") args.importForce = true;
|
|
7886
8108
|
else if (flag === "--cldr") args.importCldr = true;
|
|
8109
|
+
else if (flag === "--prune") args.prune = true;
|
|
8110
|
+
else if (flag === "--dry-run") args.dryRun = true;
|
|
7887
8111
|
else if (flag === "--rule" && next) {
|
|
7888
8112
|
args.ruleIds = next.split(",");
|
|
7889
8113
|
i++;
|
|
@@ -8313,6 +8537,57 @@ async function runImportCmd(args) {
|
|
|
8313
8537
|
saveState(out, result.state);
|
|
8314
8538
|
console.log(`Imported ${result.keyCount} keys across ${result.localeCount} locales \u2192 ${out}`);
|
|
8315
8539
|
}
|
|
8540
|
+
async function runSyncCmd(args) {
|
|
8541
|
+
const { runSync: runSync2 } = await Promise.resolve().then(() => (init_run3(), run_exports));
|
|
8542
|
+
const projectRoot = args.importSource ? resolve11(args.importSource) : dirname5(resolve11(args.statePath));
|
|
8543
|
+
if (detectFormat(args.statePath) === "none") {
|
|
8544
|
+
console.error(`No glotfile.json found at ${args.statePath}; run 'glotfile import' first.`);
|
|
8545
|
+
process.exitCode = 1;
|
|
8546
|
+
return;
|
|
8547
|
+
}
|
|
8548
|
+
let result;
|
|
8549
|
+
try {
|
|
8550
|
+
result = runSync2({
|
|
8551
|
+
projectRoot,
|
|
8552
|
+
statePath: args.statePath,
|
|
8553
|
+
format: args.importFormat,
|
|
8554
|
+
sourceLocale: args.importSourceLocale,
|
|
8555
|
+
locales: args.locales,
|
|
8556
|
+
cldr: args.importCldr,
|
|
8557
|
+
prune: args.prune
|
|
8558
|
+
});
|
|
8559
|
+
} catch (e) {
|
|
8560
|
+
console.error(e.message);
|
|
8561
|
+
process.exitCode = 1;
|
|
8562
|
+
return;
|
|
8563
|
+
}
|
|
8564
|
+
for (const w of result.warnings) console.error(`warning: ${w}`);
|
|
8565
|
+
const { plan } = result;
|
|
8566
|
+
console.log(
|
|
8567
|
+
`+${plan.added.length} added, ~${plan.sourceChanged.length} source-changed, \u2713${plan.adopted.length} adopted, -${plan.removed.length} removed${plan.removed.length && !args.prune ? " (pass --prune to delete)" : ""}.`
|
|
8568
|
+
);
|
|
8569
|
+
if (args.dryRun) {
|
|
8570
|
+
const list = (label, keys) => {
|
|
8571
|
+
if (keys.length) console.log(`
|
|
8572
|
+
${label}:
|
|
8573
|
+
${keys.join("\n ")}`);
|
|
8574
|
+
};
|
|
8575
|
+
list("Added", plan.added);
|
|
8576
|
+
list("Source changed", plan.sourceChanged);
|
|
8577
|
+
list("Adopted", plan.adopted.map((a) => `${a.key} [${a.locale}]`));
|
|
8578
|
+
list("Removed", plan.removed);
|
|
8579
|
+
console.log("\nDry run \u2014 nothing written.");
|
|
8580
|
+
return;
|
|
8581
|
+
}
|
|
8582
|
+
saveState(args.statePath, result.state);
|
|
8583
|
+
if (isLocationScannedState(result.state)) {
|
|
8584
|
+
const cache2 = refreshLocationUsage(projectRoot, args.importFormat);
|
|
8585
|
+
const refs = cache2 ? usageCounts(cache2).refs : 0;
|
|
8586
|
+
console.log(`Synced \u2192 ${args.statePath} (${result.keyCount} keys); usage index rebuilt from ${refs} location(s).`);
|
|
8587
|
+
} else {
|
|
8588
|
+
console.log(`Synced \u2192 ${args.statePath} (${result.keyCount} keys).`);
|
|
8589
|
+
}
|
|
8590
|
+
}
|
|
8316
8591
|
async function runBuildContext(args) {
|
|
8317
8592
|
const state = loadState(args.statePath);
|
|
8318
8593
|
const projectRoot = dirname5(resolve11(args.statePath));
|
|
@@ -8413,6 +8688,12 @@ async function runBuildContext(args) {
|
|
|
8413
8688
|
async function runScanCmd(args) {
|
|
8414
8689
|
const state = loadState(args.statePath);
|
|
8415
8690
|
const projectRoot = dirname5(resolve11(args.statePath));
|
|
8691
|
+
if (isLocationScannedState(state)) {
|
|
8692
|
+
const cache2 = refreshLocationUsage(projectRoot);
|
|
8693
|
+
const refs = cache2 ? usageCounts(cache2).refs : 0;
|
|
8694
|
+
console.log(`Rebuilt usage index from ${refs} catalog location(s) (code scan skipped for this format).`);
|
|
8695
|
+
return;
|
|
8696
|
+
}
|
|
8416
8697
|
const existing = loadUsageCache(projectRoot);
|
|
8417
8698
|
const result = runScan(projectRoot, state.config.scan ?? {}, existing);
|
|
8418
8699
|
const fileCount2 = Object.keys(result.files).length;
|
|
@@ -8432,10 +8713,16 @@ async function runPrune(args) {
|
|
|
8432
8713
|
}
|
|
8433
8714
|
if (args.unused) {
|
|
8434
8715
|
const projectRoot = dirname5(resolve11(args.statePath));
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8716
|
+
if (isLocationScannedState(state)) {
|
|
8717
|
+
const { runSync: runSync2 } = await Promise.resolve().then(() => (init_run3(), run_exports));
|
|
8718
|
+
const { plan } = runSync2({ projectRoot, statePath: args.statePath, prune: false });
|
|
8719
|
+
for (const k of plan.removed) toRemove.add(k);
|
|
8720
|
+
} else {
|
|
8721
|
+
const cache2 = runScan(projectRoot, state.config.scan ?? {}, loadUsageCache(projectRoot));
|
|
8722
|
+
const used = new Set(computeUsedKeys(state, cache2));
|
|
8723
|
+
for (const k of Object.keys(state.keys)) {
|
|
8724
|
+
if (!used.has(k)) toRemove.add(k);
|
|
8725
|
+
}
|
|
8439
8726
|
}
|
|
8440
8727
|
}
|
|
8441
8728
|
const keys = [...toRemove].sort();
|
|
@@ -8539,6 +8826,19 @@ var COMMAND_HELP = {
|
|
|
8539
8826
|
["--force", "Overwrite an existing glotfile.json"]
|
|
8540
8827
|
]
|
|
8541
8828
|
},
|
|
8829
|
+
sync: {
|
|
8830
|
+
summary: "Merge re-extracted locale files into the catalog, preserving glossary, context and translations.",
|
|
8831
|
+
usage: "glotfile sync [--format <name>] [--source <dir>] [--prune] [--dry-run]",
|
|
8832
|
+
options: [
|
|
8833
|
+
["--format <name>", "Source layout adapter (auto-detected if omitted)"],
|
|
8834
|
+
["--source <dir>", "Directory to read locale files from (default: the state file's directory)"],
|
|
8835
|
+
["--source-locale <code>", "Locale to treat as the source"],
|
|
8836
|
+
["--locales <list>", "Comma-separated locales to read"],
|
|
8837
|
+
["--cldr", "Expand CLDR plural forms"],
|
|
8838
|
+
["--prune", "Delete keys that are gone from the import (default: report only)"],
|
|
8839
|
+
["--dry-run", "Show the changeset without writing anything"]
|
|
8840
|
+
]
|
|
8841
|
+
},
|
|
8542
8842
|
"build-context": {
|
|
8543
8843
|
summary: "AI-generate per-key context to improve translation (requires a prior scan).",
|
|
8544
8844
|
usage: "glotfile build-context [--all] [--key <glob>] [--limit <n>] [--since <date>] [--batch]",
|
|
@@ -8635,6 +8935,7 @@ async function main(argv) {
|
|
|
8635
8935
|
if (args.command === "lint") return runLintCmd(args);
|
|
8636
8936
|
if (args.command === "check") return runCheck(args);
|
|
8637
8937
|
if (args.command === "import") return runImportCmd(args);
|
|
8938
|
+
if (args.command === "sync") return runSyncCmd(args);
|
|
8638
8939
|
if (args.command === "build-context") return runBuildContext(args);
|
|
8639
8940
|
if (args.command === "scan") return runScanCmd(args);
|
|
8640
8941
|
if (args.command === "prune") return runPrune(args);
|