glotfile 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -187,7 +187,7 @@ function validate(raw) {
187
187
  if (config.scan !== void 0) {
188
188
  const scan = config.scan;
189
189
  if (!isObject(scan)) fail("config.scan must be an object");
190
- for (const f of ["include", "exclude", "accessors", "patterns"]) {
190
+ for (const f of ["include", "exclude", "accessors", "patterns", "keep"]) {
191
191
  const v = scan[f];
192
192
  if (v !== void 0 && (!Array.isArray(v) || !v.every((x) => typeof x === "string"))) {
193
193
  fail(`config.scan.${f} must be an array of strings`);
@@ -3225,13 +3225,35 @@ function computeUsedKeys(state, cache2) {
3225
3225
  if (p.prefix) prefixes.push(p.prefix);
3226
3226
  }
3227
3227
  }
3228
- return Object.keys(state.keys).filter((key) => exact.has(key) || prefixes.some((p) => key.startsWith(p))).sort();
3228
+ const matchers = [];
3229
+ const seenLiterals = /* @__PURE__ */ new Set();
3230
+ for (const entry of Object.values(cache2.files)) {
3231
+ for (const l of entry.literals ?? []) {
3232
+ if (!l.literal || seenLiterals.has(l.literal)) continue;
3233
+ seenLiterals.add(l.literal);
3234
+ matchers.push(literalMatcher(l.literal));
3235
+ }
3236
+ }
3237
+ const keep = (state.config.scan?.keep ?? []).map(globToRegExp);
3238
+ return Object.keys(state.keys).filter((key) => exact.has(key) || prefixes.some((p) => key.startsWith(p)) || matchers.some((matches) => matches(key)) || keep.some((re) => re.test(key))).sort();
3239
+ }
3240
+ function escapeRe(s) {
3241
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3242
+ }
3243
+ function literalMatcher(literal) {
3244
+ if (/%[sd]/.test(literal)) {
3245
+ const re = new RegExp(`^${literal.split(/%[sd]/).map(escapeRe).join("[^.]+")}$`);
3246
+ return (key) => re.test(key);
3247
+ }
3248
+ if (literal.endsWith(".")) return (key) => key.startsWith(literal);
3249
+ return (key) => key === literal || key.startsWith(literal + ".");
3229
3250
  }
3230
3251
  var init_scan = __esm({
3231
3252
  "src/server/scan.ts"() {
3232
3253
  "use strict";
3233
3254
  init_atomic_write();
3234
3255
  init_glotfile_dir();
3256
+ init_glob();
3235
3257
  }
3236
3258
  });
3237
3259
 
@@ -3241,7 +3263,7 @@ import { join as join3, extname as extname2, relative } from "path";
3241
3263
  function scannerForExt(ext) {
3242
3264
  return EXT_SCANNER[ext] ?? null;
3243
3265
  }
3244
- function escapeRe(s) {
3266
+ function escapeRe2(s) {
3245
3267
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3246
3268
  }
3247
3269
  function detectFlutterAccessors(content) {
@@ -3258,7 +3280,7 @@ function flutterPatterns(content, opts) {
3258
3280
  ...FLUTTER_ACCESSOR_DEFAULTS,
3259
3281
  ...detectFlutterAccessors(content),
3260
3282
  ...opts?.accessors ?? []
3261
- ])].map(escapeRe);
3283
+ ])].map(escapeRe2);
3262
3284
  return [
3263
3285
  // AppLocalizations.of(context)!.key — tolerates the !/? null-assertion.
3264
3286
  /AppLocalizations\.of\([^)]*\)[!?]?\.([a-zA-Z_][a-zA-Z0-9_]*)/g,
@@ -3343,6 +3365,32 @@ function extractPrefixes(content, scanner) {
3343
3365
  result.sort((a, b) => a.line - b.line || a.col - b.col);
3344
3366
  return result;
3345
3367
  }
3368
+ function extractLiterals(content) {
3369
+ const starts = lineStartOffsets(content);
3370
+ const result = [];
3371
+ const seen = /* @__PURE__ */ new Set();
3372
+ for (const pattern of STRING_LITERALS) {
3373
+ const re = new RegExp(pattern.source, "g");
3374
+ let m;
3375
+ while ((m = re.exec(content)) !== null) {
3376
+ let text = m[1];
3377
+ const marker = text.search(/\{\$|\$\{/);
3378
+ if (marker !== -1) {
3379
+ text = text.slice(0, marker);
3380
+ if (!text.endsWith(".")) continue;
3381
+ }
3382
+ if (!KEY_SHAPE.test(text)) continue;
3383
+ const { line, col } = offsetToLineCol(starts, m.index);
3384
+ const dedup = `${line}:${col}:${text}`;
3385
+ if (!seen.has(dedup)) {
3386
+ seen.add(dedup);
3387
+ result.push({ literal: text, line, col });
3388
+ }
3389
+ }
3390
+ }
3391
+ result.sort((a, b) => a.line - b.line || a.col - b.col);
3392
+ return result;
3393
+ }
3346
3394
  function matchesGlob(relPath, glob) {
3347
3395
  const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(//g, ".*");
3348
3396
  return new RegExp(`^${escaped}$`).test(relPath);
@@ -3417,13 +3465,14 @@ function runScan(projectRoot, opts, existing) {
3417
3465
  mtime,
3418
3466
  size,
3419
3467
  refs: extractRefs(content, scanner, opts),
3420
- prefixes: extractPrefixes(content, scanner)
3468
+ prefixes: extractPrefixes(content, scanner),
3469
+ literals: extractLiterals(content)
3421
3470
  };
3422
3471
  }
3423
3472
  saveUsageCache(projectRoot, cache2);
3424
3473
  return cache2;
3425
3474
  }
3426
- var PATTERNS, PREFIX_PATTERNS, CACHE_VERSION, EXT_SCANNER, ALWAYS_EXCLUDE, FLUTTER_ACCESSOR_DEFAULTS;
3475
+ var PATTERNS, PREFIX_PATTERNS, CACHE_VERSION, EXT_SCANNER, ALWAYS_EXCLUDE, FLUTTER_ACCESSOR_DEFAULTS, KEY_SHAPE, STRING_LITERALS;
3427
3476
  var init_scanner = __esm({
3428
3477
  "src/server/scanner.ts"() {
3429
3478
  "use strict";
@@ -3480,7 +3529,7 @@ var init_scanner = __esm({
3480
3529
  /(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$]*)\$\{/g
3481
3530
  ]
3482
3531
  };
3483
- CACHE_VERSION = 5;
3532
+ CACHE_VERSION = 6;
3484
3533
  EXT_SCANNER = {
3485
3534
  ".php": "laravel",
3486
3535
  ".vue": "js-i18n",
@@ -3514,6 +3563,12 @@ var init_scanner = __esm({
3514
3563
  "__pycache__"
3515
3564
  ]);
3516
3565
  FLUTTER_ACCESSOR_DEFAULTS = ["l10n", "loc", "localizations", "translations"];
3566
+ KEY_SHAPE = /^[A-Za-z0-9_][A-Za-z0-9_/-]*(?:\.(?:[A-Za-z0-9_-]+|%[sd]))+\.?$/;
3567
+ STRING_LITERALS = [
3568
+ /'([^'\\\n]+)'/g,
3569
+ /"([^"\\\n]+)"/g,
3570
+ /`([^`\\\n]+)`/g
3571
+ ];
3517
3572
  }
3518
3573
  });
3519
3574
 
@@ -6726,23 +6781,34 @@ function createApi(deps) {
6726
6781
  app.get("/scan/usage", (c) => {
6727
6782
  const key = c.req.query("key") ?? "";
6728
6783
  const cache2 = loadUsageCache(projectRoot);
6729
- if (!cache2) return c.json({ indexed: false, count: 0, refs: [], prefixCount: 0, prefixRefs: [] });
6784
+ if (!cache2) return c.json({ indexed: false, count: 0, refs: [], prefixCount: 0, prefixRefs: [], literalCount: 0, literalRefs: [] });
6730
6785
  const refs = [];
6731
6786
  const prefixRefs = [];
6787
+ const literalRefs = [];
6732
6788
  for (const [file, entry] of Object.entries(cache2.files)) {
6733
6789
  const abs = resolve9(projectRoot, file);
6790
+ const refLines = /* @__PURE__ */ new Set();
6734
6791
  for (const r of entry.refs) {
6735
- if (r.key === key) refs.push({ file, abs, line: r.line, col: r.col, scanner: r.scanner });
6792
+ if (r.key === key) {
6793
+ refs.push({ file, abs, line: r.line, col: r.col, scanner: r.scanner });
6794
+ refLines.add(r.line);
6795
+ }
6736
6796
  }
6737
6797
  for (const p of entry.prefixes) {
6738
6798
  if (key.startsWith(p.prefix)) {
6739
6799
  prefixRefs.push({ file, abs, line: p.line, col: p.col, scanner: p.scanner, prefix: p.prefix });
6740
6800
  }
6741
6801
  }
6802
+ for (const l of entry.literals ?? []) {
6803
+ if (literalMatcher(l.literal)(key) && !refLines.has(l.line)) {
6804
+ literalRefs.push({ file, abs, line: l.line, col: l.col, literal: l.literal });
6805
+ }
6806
+ }
6742
6807
  }
6743
6808
  const byFileLine = (a, b) => a.file.localeCompare(b.file) || a.line - b.line;
6744
6809
  refs.sort(byFileLine);
6745
6810
  prefixRefs.sort(byFileLine);
6811
+ literalRefs.sort(byFileLine);
6746
6812
  return c.json({
6747
6813
  indexed: true,
6748
6814
  scannedAt: cache2.scannedAt,
@@ -6750,7 +6816,9 @@ function createApi(deps) {
6750
6816
  count: refs.length,
6751
6817
  refs,
6752
6818
  prefixCount: prefixRefs.length,
6753
- prefixRefs
6819
+ prefixRefs,
6820
+ literalCount: literalRefs.length,
6821
+ literalRefs
6754
6822
  });
6755
6823
  });
6756
6824
  app.get("/scan/used", (c) => {
@@ -186,7 +186,7 @@ function validate(raw) {
186
186
  if (config.scan !== void 0) {
187
187
  const scan = config.scan;
188
188
  if (!isObject(scan)) fail("config.scan must be an object");
189
- for (const f of ["include", "exclude", "accessors", "patterns"]) {
189
+ for (const f of ["include", "exclude", "accessors", "patterns", "keep"]) {
190
190
  const v = scan[f];
191
191
  if (v !== void 0 && (!Array.isArray(v) || !v.every((x) => typeof x === "string"))) {
192
192
  fail(`config.scan.${f} must be an array of strings`);
@@ -817,6 +817,12 @@ function ensureGlotfileDir(projectRoot) {
817
817
  return dir;
818
818
  }
819
819
 
820
+ // src/server/glob.ts
821
+ function globToRegExp(glob) {
822
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
823
+ return new RegExp(`^${escaped}$`);
824
+ }
825
+
820
826
  // src/server/scan.ts
821
827
  function loadUsageCache(projectRoot) {
822
828
  const path = resolve2(projectRoot, ".glotfile", "usage.json");
@@ -854,7 +860,28 @@ function computeUsedKeys(state, cache2) {
854
860
  if (p.prefix) prefixes.push(p.prefix);
855
861
  }
856
862
  }
857
- return Object.keys(state.keys).filter((key) => exact.has(key) || prefixes.some((p) => key.startsWith(p))).sort();
863
+ const matchers = [];
864
+ const seenLiterals = /* @__PURE__ */ new Set();
865
+ for (const entry of Object.values(cache2.files)) {
866
+ for (const l of entry.literals ?? []) {
867
+ if (!l.literal || seenLiterals.has(l.literal)) continue;
868
+ seenLiterals.add(l.literal);
869
+ matchers.push(literalMatcher(l.literal));
870
+ }
871
+ }
872
+ const keep = (state.config.scan?.keep ?? []).map(globToRegExp);
873
+ return Object.keys(state.keys).filter((key) => exact.has(key) || prefixes.some((p) => key.startsWith(p)) || matchers.some((matches) => matches(key)) || keep.some((re) => re.test(key))).sort();
874
+ }
875
+ function escapeRe(s) {
876
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
877
+ }
878
+ function literalMatcher(literal) {
879
+ if (/%[sd]/.test(literal)) {
880
+ const re = new RegExp(`^${literal.split(/%[sd]/).map(escapeRe).join("[^.]+")}$`);
881
+ return (key) => re.test(key);
882
+ }
883
+ if (literal.endsWith(".")) return (key) => key.startsWith(literal);
884
+ return (key) => key === literal || key.startsWith(literal + ".");
858
885
  }
859
886
 
860
887
  // src/server/scanner.ts
@@ -912,7 +939,7 @@ var PREFIX_PATTERNS = {
912
939
  /(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$]*)\$\{/g
913
940
  ]
914
941
  };
915
- var CACHE_VERSION = 5;
942
+ var CACHE_VERSION = 6;
916
943
  var EXT_SCANNER = {
917
944
  ".php": "laravel",
918
945
  ".vue": "js-i18n",
@@ -949,7 +976,7 @@ function scannerForExt(ext) {
949
976
  return EXT_SCANNER[ext] ?? null;
950
977
  }
951
978
  var FLUTTER_ACCESSOR_DEFAULTS = ["l10n", "loc", "localizations", "translations"];
952
- function escapeRe(s) {
979
+ function escapeRe2(s) {
953
980
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
954
981
  }
955
982
  function detectFlutterAccessors(content) {
@@ -966,7 +993,7 @@ function flutterPatterns(content, opts) {
966
993
  ...FLUTTER_ACCESSOR_DEFAULTS,
967
994
  ...detectFlutterAccessors(content),
968
995
  ...opts?.accessors ?? []
969
- ])].map(escapeRe);
996
+ ])].map(escapeRe2);
970
997
  return [
971
998
  // AppLocalizations.of(context)!.key — tolerates the !/? null-assertion.
972
999
  /AppLocalizations\.of\([^)]*\)[!?]?\.([a-zA-Z_][a-zA-Z0-9_]*)/g,
@@ -1051,6 +1078,38 @@ function extractPrefixes(content, scanner) {
1051
1078
  result.sort((a, b) => a.line - b.line || a.col - b.col);
1052
1079
  return result;
1053
1080
  }
1081
+ var KEY_SHAPE = /^[A-Za-z0-9_][A-Za-z0-9_/-]*(?:\.(?:[A-Za-z0-9_-]+|%[sd]))+\.?$/;
1082
+ var STRING_LITERALS = [
1083
+ /'([^'\\\n]+)'/g,
1084
+ /"([^"\\\n]+)"/g,
1085
+ /`([^`\\\n]+)`/g
1086
+ ];
1087
+ function extractLiterals(content) {
1088
+ const starts = lineStartOffsets(content);
1089
+ const result = [];
1090
+ const seen = /* @__PURE__ */ new Set();
1091
+ for (const pattern of STRING_LITERALS) {
1092
+ const re = new RegExp(pattern.source, "g");
1093
+ let m;
1094
+ while ((m = re.exec(content)) !== null) {
1095
+ let text = m[1];
1096
+ const marker = text.search(/\{\$|\$\{/);
1097
+ if (marker !== -1) {
1098
+ text = text.slice(0, marker);
1099
+ if (!text.endsWith(".")) continue;
1100
+ }
1101
+ if (!KEY_SHAPE.test(text)) continue;
1102
+ const { line, col } = offsetToLineCol(starts, m.index);
1103
+ const dedup = `${line}:${col}:${text}`;
1104
+ if (!seen.has(dedup)) {
1105
+ seen.add(dedup);
1106
+ result.push({ literal: text, line, col });
1107
+ }
1108
+ }
1109
+ }
1110
+ result.sort((a, b) => a.line - b.line || a.col - b.col);
1111
+ return result;
1112
+ }
1054
1113
  function matchesGlob(relPath, glob) {
1055
1114
  const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(//g, ".*");
1056
1115
  return new RegExp(`^${escaped}$`).test(relPath);
@@ -1125,7 +1184,8 @@ function runScan(projectRoot, opts, existing) {
1125
1184
  mtime,
1126
1185
  size,
1127
1186
  refs: extractRefs(content, scanner, opts),
1128
- prefixes: extractPrefixes(content, scanner)
1187
+ prefixes: extractPrefixes(content, scanner),
1188
+ literals: extractLiterals(content)
1129
1189
  };
1130
1190
  }
1131
1191
  saveUsageCache(projectRoot, cache2);
@@ -1139,7 +1199,7 @@ var MAX_CONTEXT_LENGTH = 500;
1139
1199
  var SNIPPET_WINDOW = 15;
1140
1200
  var MAX_SNIPPETS = 3;
1141
1201
  var EXCLUDED_DIRS = ["node_modules/", "vendor/", "dist/", ".git/", ".glotfile/"];
1142
- function globToRegExp(glob) {
1202
+ function globToRegExp2(glob) {
1143
1203
  const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
1144
1204
  return new RegExp(`^${escaped}$`);
1145
1205
  }
@@ -1182,7 +1242,7 @@ function buildUsageIndex(cache2) {
1182
1242
  }
1183
1243
  function selectContextTargets(state, opts, cache2, lastRunAt) {
1184
1244
  const cutoff = opts.all ? void 0 : opts.since ?? lastRunAt;
1185
- const keyRe = opts.keyGlob ? globToRegExp(opts.keyGlob) : null;
1245
+ const keyRe = opts.keyGlob ? globToRegExp2(opts.keyGlob) : null;
1186
1246
  const keySet = opts.keys ? new Set(opts.keys) : null;
1187
1247
  const usageIndex = buildUsageIndex(cache2);
1188
1248
  let candidates = [];
@@ -1678,12 +1738,6 @@ function runChecks(state, opts = {}) {
1678
1738
  return { issues: visible, spellPending };
1679
1739
  }
1680
1740
 
1681
- // src/server/glob.ts
1682
- function globToRegExp2(glob) {
1683
- const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
1684
- return new RegExp(`^${escaped}$`);
1685
- }
1686
-
1687
1741
  // src/server/lint/spelling.ts
1688
1742
  var spellingRule = {
1689
1743
  id: "spelling",
@@ -1933,7 +1987,7 @@ async function runLint(state, options = {}) {
1933
1987
  spellers,
1934
1988
  allowWords
1935
1989
  };
1936
- const ignoreRes = (config.ignore ?? []).map(globToRegExp2);
1990
+ const ignoreRes = (config.ignore ?? []).map(globToRegExp);
1937
1991
  const localeFilter = options.locales ? new Set(options.locales) : null;
1938
1992
  const findings = [];
1939
1993
  let suppressed = 0;
@@ -3461,7 +3515,7 @@ import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
3461
3515
  import { resolve as resolve5, extname as extname2 } from "path";
3462
3516
  function selectRequests(state, opts) {
3463
3517
  const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
3464
- const keyRe = opts.keyGlob ? globToRegExp2(opts.keyGlob) : null;
3518
+ const keyRe = opts.keyGlob ? globToRegExp(opts.keyGlob) : null;
3465
3519
  const keySet = opts.keys ? new Set(opts.keys) : null;
3466
3520
  const reqs = [];
3467
3521
  let id = 0;
@@ -6198,23 +6252,34 @@ function createApi(deps) {
6198
6252
  app.get("/scan/usage", (c) => {
6199
6253
  const key = c.req.query("key") ?? "";
6200
6254
  const cache2 = loadUsageCache(projectRoot);
6201
- if (!cache2) return c.json({ indexed: false, count: 0, refs: [], prefixCount: 0, prefixRefs: [] });
6255
+ if (!cache2) return c.json({ indexed: false, count: 0, refs: [], prefixCount: 0, prefixRefs: [], literalCount: 0, literalRefs: [] });
6202
6256
  const refs = [];
6203
6257
  const prefixRefs = [];
6258
+ const literalRefs = [];
6204
6259
  for (const [file, entry] of Object.entries(cache2.files)) {
6205
6260
  const abs = resolve9(projectRoot, file);
6261
+ const refLines = /* @__PURE__ */ new Set();
6206
6262
  for (const r of entry.refs) {
6207
- if (r.key === key) refs.push({ file, abs, line: r.line, col: r.col, scanner: r.scanner });
6263
+ if (r.key === key) {
6264
+ refs.push({ file, abs, line: r.line, col: r.col, scanner: r.scanner });
6265
+ refLines.add(r.line);
6266
+ }
6208
6267
  }
6209
6268
  for (const p of entry.prefixes) {
6210
6269
  if (key.startsWith(p.prefix)) {
6211
6270
  prefixRefs.push({ file, abs, line: p.line, col: p.col, scanner: p.scanner, prefix: p.prefix });
6212
6271
  }
6213
6272
  }
6273
+ for (const l of entry.literals ?? []) {
6274
+ if (literalMatcher(l.literal)(key) && !refLines.has(l.line)) {
6275
+ literalRefs.push({ file, abs, line: l.line, col: l.col, literal: l.literal });
6276
+ }
6277
+ }
6214
6278
  }
6215
6279
  const byFileLine = (a, b) => a.file.localeCompare(b.file) || a.line - b.line;
6216
6280
  refs.sort(byFileLine);
6217
6281
  prefixRefs.sort(byFileLine);
6282
+ literalRefs.sort(byFileLine);
6218
6283
  return c.json({
6219
6284
  indexed: true,
6220
6285
  scannedAt: cache2.scannedAt,
@@ -6222,7 +6287,9 @@ function createApi(deps) {
6222
6287
  count: refs.length,
6223
6288
  refs,
6224
6289
  prefixCount: prefixRefs.length,
6225
- prefixRefs
6290
+ prefixRefs,
6291
+ literalCount: literalRefs.length,
6292
+ literalRefs
6226
6293
  });
6227
6294
  });
6228
6295
  app.get("/scan/used", (c) => {