glotfile 0.6.2 → 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.
@@ -3225,8 +3225,28 @@ function computeUsedKeys(state, cache2) {
3225
3225
  if (p.prefix) prefixes.push(p.prefix);
3226
3226
  }
3227
3227
  }
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
+ }
3228
3237
  const keep = (state.config.scan?.keep ?? []).map(globToRegExp);
3229
- return Object.keys(state.keys).filter((key) => exact.has(key) || prefixes.some((p) => key.startsWith(p)) || keep.some((re) => re.test(key))).sort();
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 + ".");
3230
3250
  }
3231
3251
  var init_scan = __esm({
3232
3252
  "src/server/scan.ts"() {
@@ -3243,7 +3263,7 @@ import { join as join3, extname as extname2, relative } from "path";
3243
3263
  function scannerForExt(ext) {
3244
3264
  return EXT_SCANNER[ext] ?? null;
3245
3265
  }
3246
- function escapeRe(s) {
3266
+ function escapeRe2(s) {
3247
3267
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3248
3268
  }
3249
3269
  function detectFlutterAccessors(content) {
@@ -3260,7 +3280,7 @@ function flutterPatterns(content, opts) {
3260
3280
  ...FLUTTER_ACCESSOR_DEFAULTS,
3261
3281
  ...detectFlutterAccessors(content),
3262
3282
  ...opts?.accessors ?? []
3263
- ])].map(escapeRe);
3283
+ ])].map(escapeRe2);
3264
3284
  return [
3265
3285
  // AppLocalizations.of(context)!.key — tolerates the !/? null-assertion.
3266
3286
  /AppLocalizations\.of\([^)]*\)[!?]?\.([a-zA-Z_][a-zA-Z0-9_]*)/g,
@@ -3345,6 +3365,32 @@ function extractPrefixes(content, scanner) {
3345
3365
  result.sort((a, b) => a.line - b.line || a.col - b.col);
3346
3366
  return result;
3347
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
+ }
3348
3394
  function matchesGlob(relPath, glob) {
3349
3395
  const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(//g, ".*");
3350
3396
  return new RegExp(`^${escaped}$`).test(relPath);
@@ -3419,13 +3465,14 @@ function runScan(projectRoot, opts, existing) {
3419
3465
  mtime,
3420
3466
  size,
3421
3467
  refs: extractRefs(content, scanner, opts),
3422
- prefixes: extractPrefixes(content, scanner)
3468
+ prefixes: extractPrefixes(content, scanner),
3469
+ literals: extractLiterals(content)
3423
3470
  };
3424
3471
  }
3425
3472
  saveUsageCache(projectRoot, cache2);
3426
3473
  return cache2;
3427
3474
  }
3428
- 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;
3429
3476
  var init_scanner = __esm({
3430
3477
  "src/server/scanner.ts"() {
3431
3478
  "use strict";
@@ -3482,7 +3529,7 @@ var init_scanner = __esm({
3482
3529
  /(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$]*)\$\{/g
3483
3530
  ]
3484
3531
  };
3485
- CACHE_VERSION = 5;
3532
+ CACHE_VERSION = 6;
3486
3533
  EXT_SCANNER = {
3487
3534
  ".php": "laravel",
3488
3535
  ".vue": "js-i18n",
@@ -3516,6 +3563,12 @@ var init_scanner = __esm({
3516
3563
  "__pycache__"
3517
3564
  ]);
3518
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
+ ];
3519
3572
  }
3520
3573
  });
3521
3574
 
@@ -6728,23 +6781,34 @@ function createApi(deps) {
6728
6781
  app.get("/scan/usage", (c) => {
6729
6782
  const key = c.req.query("key") ?? "";
6730
6783
  const cache2 = loadUsageCache(projectRoot);
6731
- 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: [] });
6732
6785
  const refs = [];
6733
6786
  const prefixRefs = [];
6787
+ const literalRefs = [];
6734
6788
  for (const [file, entry] of Object.entries(cache2.files)) {
6735
6789
  const abs = resolve9(projectRoot, file);
6790
+ const refLines = /* @__PURE__ */ new Set();
6736
6791
  for (const r of entry.refs) {
6737
- 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
+ }
6738
6796
  }
6739
6797
  for (const p of entry.prefixes) {
6740
6798
  if (key.startsWith(p.prefix)) {
6741
6799
  prefixRefs.push({ file, abs, line: p.line, col: p.col, scanner: p.scanner, prefix: p.prefix });
6742
6800
  }
6743
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
+ }
6744
6807
  }
6745
6808
  const byFileLine = (a, b) => a.file.localeCompare(b.file) || a.line - b.line;
6746
6809
  refs.sort(byFileLine);
6747
6810
  prefixRefs.sort(byFileLine);
6811
+ literalRefs.sort(byFileLine);
6748
6812
  return c.json({
6749
6813
  indexed: true,
6750
6814
  scannedAt: cache2.scannedAt,
@@ -6752,7 +6816,9 @@ function createApi(deps) {
6752
6816
  count: refs.length,
6753
6817
  refs,
6754
6818
  prefixCount: prefixRefs.length,
6755
- prefixRefs
6819
+ prefixRefs,
6820
+ literalCount: literalRefs.length,
6821
+ literalRefs
6756
6822
  });
6757
6823
  });
6758
6824
  app.get("/scan/used", (c) => {
@@ -860,8 +860,28 @@ function computeUsedKeys(state, cache2) {
860
860
  if (p.prefix) prefixes.push(p.prefix);
861
861
  }
862
862
  }
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
+ }
863
872
  const keep = (state.config.scan?.keep ?? []).map(globToRegExp);
864
- return Object.keys(state.keys).filter((key) => exact.has(key) || prefixes.some((p) => key.startsWith(p)) || keep.some((re) => re.test(key))).sort();
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 + ".");
865
885
  }
866
886
 
867
887
  // src/server/scanner.ts
@@ -919,7 +939,7 @@ var PREFIX_PATTERNS = {
919
939
  /(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$]*)\$\{/g
920
940
  ]
921
941
  };
922
- var CACHE_VERSION = 5;
942
+ var CACHE_VERSION = 6;
923
943
  var EXT_SCANNER = {
924
944
  ".php": "laravel",
925
945
  ".vue": "js-i18n",
@@ -956,7 +976,7 @@ function scannerForExt(ext) {
956
976
  return EXT_SCANNER[ext] ?? null;
957
977
  }
958
978
  var FLUTTER_ACCESSOR_DEFAULTS = ["l10n", "loc", "localizations", "translations"];
959
- function escapeRe(s) {
979
+ function escapeRe2(s) {
960
980
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
961
981
  }
962
982
  function detectFlutterAccessors(content) {
@@ -973,7 +993,7 @@ function flutterPatterns(content, opts) {
973
993
  ...FLUTTER_ACCESSOR_DEFAULTS,
974
994
  ...detectFlutterAccessors(content),
975
995
  ...opts?.accessors ?? []
976
- ])].map(escapeRe);
996
+ ])].map(escapeRe2);
977
997
  return [
978
998
  // AppLocalizations.of(context)!.key — tolerates the !/? null-assertion.
979
999
  /AppLocalizations\.of\([^)]*\)[!?]?\.([a-zA-Z_][a-zA-Z0-9_]*)/g,
@@ -1058,6 +1078,38 @@ function extractPrefixes(content, scanner) {
1058
1078
  result.sort((a, b) => a.line - b.line || a.col - b.col);
1059
1079
  return result;
1060
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
+ }
1061
1113
  function matchesGlob(relPath, glob) {
1062
1114
  const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(//g, ".*");
1063
1115
  return new RegExp(`^${escaped}$`).test(relPath);
@@ -1132,7 +1184,8 @@ function runScan(projectRoot, opts, existing) {
1132
1184
  mtime,
1133
1185
  size,
1134
1186
  refs: extractRefs(content, scanner, opts),
1135
- prefixes: extractPrefixes(content, scanner)
1187
+ prefixes: extractPrefixes(content, scanner),
1188
+ literals: extractLiterals(content)
1136
1189
  };
1137
1190
  }
1138
1191
  saveUsageCache(projectRoot, cache2);
@@ -6199,23 +6252,34 @@ function createApi(deps) {
6199
6252
  app.get("/scan/usage", (c) => {
6200
6253
  const key = c.req.query("key") ?? "";
6201
6254
  const cache2 = loadUsageCache(projectRoot);
6202
- 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: [] });
6203
6256
  const refs = [];
6204
6257
  const prefixRefs = [];
6258
+ const literalRefs = [];
6205
6259
  for (const [file, entry] of Object.entries(cache2.files)) {
6206
6260
  const abs = resolve9(projectRoot, file);
6261
+ const refLines = /* @__PURE__ */ new Set();
6207
6262
  for (const r of entry.refs) {
6208
- 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
+ }
6209
6267
  }
6210
6268
  for (const p of entry.prefixes) {
6211
6269
  if (key.startsWith(p.prefix)) {
6212
6270
  prefixRefs.push({ file, abs, line: p.line, col: p.col, scanner: p.scanner, prefix: p.prefix });
6213
6271
  }
6214
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
+ }
6215
6278
  }
6216
6279
  const byFileLine = (a, b) => a.file.localeCompare(b.file) || a.line - b.line;
6217
6280
  refs.sort(byFileLine);
6218
6281
  prefixRefs.sort(byFileLine);
6282
+ literalRefs.sort(byFileLine);
6219
6283
  return c.json({
6220
6284
  indexed: true,
6221
6285
  scannedAt: cache2.scannedAt,
@@ -6223,7 +6287,9 @@ function createApi(deps) {
6223
6287
  count: refs.length,
6224
6288
  refs,
6225
6289
  prefixCount: prefixRefs.length,
6226
- prefixRefs
6290
+ prefixRefs,
6291
+ literalCount: literalRefs.length,
6292
+ literalRefs
6227
6293
  });
6228
6294
  });
6229
6295
  app.get("/scan/used", (c) => {