@the-i18n-kit/cli 7.0.0 → 8.0.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.
Files changed (27) hide show
  1. package/dist/bin.js +1 -1
  2. package/dist/{cli-waEjbUM1.js → cli-BTMcWXGs.js} +3 -3
  3. package/dist/{cli-waEjbUM1.js.map → cli-BTMcWXGs.js.map} +1 -1
  4. package/dist/{define-config-e0B0VHq7.d.ts → define-config-BpdVaEVR.d.ts} +1 -1
  5. package/dist/{define-config-ChY3jk6K.d.ts → define-config-ZRZw5PWy.d.ts} +15 -3
  6. package/dist/define-config-ZRZw5PWy.d.ts.map +1 -0
  7. package/dist/define-config.d.ts +1 -1
  8. package/dist/{descriptors-10sgyqEs.js → descriptors-Bo5UM031.js} +23 -6
  9. package/dist/{descriptors-10sgyqEs.js.map → descriptors-Bo5UM031.js.map} +1 -1
  10. package/dist/{detector-DtFY4qm3.js → detector-B4z_RZde.js} +2 -2
  11. package/dist/{detector-DtFY4qm3.js.map → detector-B4z_RZde.js.map} +1 -1
  12. package/dist/{detector-BJTkyhQ0.js → detector-DTfFbwSU.js} +2 -2
  13. package/dist/{index-CdjJ71Ag.d.ts → index-CWFWYHf5.d.ts} +76 -50
  14. package/dist/index-CWFWYHf5.d.ts.map +1 -0
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +5 -5
  17. package/dist/operations-CKHEmLNJ.js +8 -0
  18. package/dist/{operations-CNqEBD__.js → operations-Daf2Ommm.js} +183 -46
  19. package/dist/operations-Daf2Ommm.js.map +1 -0
  20. package/dist/{project-config-C3ao4Uii.js → project-config-eLR0J377.js} +6 -2
  21. package/dist/project-config-eLR0J377.js.map +1 -0
  22. package/package.json +1 -1
  23. package/dist/define-config-ChY3jk6K.d.ts.map +0 -1
  24. package/dist/index-CdjJ71Ag.d.ts.map +0 -1
  25. package/dist/operations-CNqEBD__.js.map +0 -1
  26. package/dist/operations-D7IMu_xY.js +0 -8
  27. package/dist/project-config-C3ao4Uii.js.map +0 -1
@@ -1,8 +1,8 @@
1
1
  import { i as toErrorMessage, n as FileIOError, r as ToolError } from "./errors-coI1dhw1.js";
2
2
  import { c as findLocaleOrThrow, d as findWritableLayerOrThrow, f as localeRefInfo, h as resolveReferenceLocale, l as findLocaleSuggestion, m as resolveLocaleRef, n as TranslateProviderError, o as findLayerOrThrow, p as resolveLayersToScan, s as findLocaleImpl, u as findReferenceLocaleOrThrow } from "./providers-BbVPelvp.js";
3
- import { i as log, r as validateProjectConfig, t as CONFIG_FILENAME } from "./project-config-C3ao4Uii.js";
3
+ import { i as log, r as validateProjectConfig, t as CONFIG_FILENAME } from "./project-config-eLR0J377.js";
4
4
  import { a as atomicWrite, c as hasNestedKey, d as renameNestedKey, f as setNestedValue, m as validateTranslationValue, o as getLeafKeys, r as writeLocaleFile, s as getNestedValue, u as removeNestedValue } from "./json-writer-ek8yEI5R.js";
5
- import { a as detectFrameworkMatch, n as clearConfigCache, o as formatForFile, s as getFormat, t as detectI18nConfig } from "./detector-DtFY4qm3.js";
5
+ import { a as detectFrameworkMatch, n as clearConfigCache, o as formatForFile, s as getFormat, t as detectI18nConfig } from "./detector-B4z_RZde.js";
6
6
  import { createRequire } from "node:module";
7
7
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
8
  import { mkdir, readFile, readdir, unlink } from "node:fs/promises";
@@ -2169,13 +2169,86 @@ async function collectEmptyTranslations(config, opts) {
2169
2169
  };
2170
2170
  }
2171
2171
  /**
2172
- * Search translation files by key pattern or value substring.
2172
+ * Everything a comparison should ignore when the question is whether a
2173
+ * translation for some text already exists: case, accents, punctuation and how
2174
+ * much whitespace sits between the words. "Save changes!" and "save changes"
2175
+ * are the same phrase to whoever is deciding whether to reuse the key.
2176
+ */
2177
+ function normalizeForMatch(text) {
2178
+ return text.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
2179
+ }
2180
+ /**
2181
+ * How much of two normalized strings is the same words (Sørensen–Dice over
2182
+ * tokens). Word order and the words neither side shares are what it ignores,
2183
+ * which is what "Save your changes" and "Changes saved" need it to ignore —
2184
+ * and what keeps "Save" away from "Delete".
2185
+ */
2186
+ function tokenSimilarity(a, b) {
2187
+ const left = new Set(a.split(" ").filter(Boolean));
2188
+ const right = new Set(b.split(" ").filter(Boolean));
2189
+ if (left.size === 0 || right.size === 0) return 0;
2190
+ let shared = 0;
2191
+ for (const token of left) if (right.has(token)) shared++;
2192
+ return 2 * shared / (left.size + right.size);
2193
+ }
2194
+ /**
2195
+ * Where a fuzzy match stops being one. Fixed rather than a parameter: a caller
2196
+ * cannot calibrate a number it never sees the scores behind, and a threshold
2197
+ * that moves would make the same query answer differently between runs.
2198
+ */
2199
+ const FUZZY_THRESHOLD = .6;
2200
+ /**
2201
+ * The comparison one search runs against every key path and value, built once
2202
+ * so the query is normalized once instead of per candidate.
2203
+ */
2204
+ function buildMatcher(matchMode, query) {
2205
+ if (matchMode === "contains") {
2206
+ const needle = query.toLowerCase();
2207
+ return (candidate) => candidate.toLowerCase().includes(needle);
2208
+ }
2209
+ const normalizedQuery = normalizeForMatch(query);
2210
+ if (matchMode === "exact") return (candidate) => normalizeForMatch(candidate) === normalizedQuery;
2211
+ return (candidate) => {
2212
+ if (normalizedQuery === "") return false;
2213
+ const normalized = normalizeForMatch(candidate);
2214
+ return normalized.includes(normalizedQuery) || tokenSimilarity(normalizedQuery, normalized) >= FUZZY_THRESHOLD;
2215
+ };
2216
+ }
2217
+ /**
2218
+ * Collapse the detail rows to one row per key.
2219
+ *
2220
+ * `layers` and `localeCount` are counted over every sheet in scope rather than
2221
+ * over the rows that matched: a key whose German value matched is still defined
2222
+ * in the other six layers, and that is the fact the caller is asking for.
2223
+ */
2224
+ function groupMatchesByKey(matches, sheets, referenceCode) {
2225
+ const grouped = [];
2226
+ for (const key of new Set(matches.map((match) => match.key))) {
2227
+ const defining = sheets.filter((sheet) => getNestedValue(sheet.data, key) !== void 0);
2228
+ const source = defining.find((sheet) => sheet.locale === referenceCode) ?? defining[0];
2229
+ if (!source) continue;
2230
+ grouped.push({
2231
+ key,
2232
+ layers: [...new Set(defining.map((sheet) => sheet.layer))],
2233
+ value: getNestedValue(source.data, key),
2234
+ locale: source.locale,
2235
+ localeCount: new Set(defining.map((sheet) => sheet.locale)).size
2236
+ });
2237
+ }
2238
+ return grouped;
2239
+ }
2240
+ /**
2241
+ * Search translation files by key path or value.
2242
+ *
2243
+ * Returns one row per key. The detail rows — one per key and locale — are what
2244
+ * `includeLocales` asks for.
2173
2245
  */
2174
2246
  async function searchTranslations(opts) {
2175
2247
  const { query, layer, locale } = opts;
2176
2248
  const config = await detectI18nConfig(opts.projectDir ?? process.cwd());
2177
2249
  const mode = opts.searchIn ?? "both";
2178
- const queryLower = query.toLowerCase();
2250
+ const matchMode = opts.matchMode ?? "contains";
2251
+ const isMatch = buildMatcher(matchMode, query);
2179
2252
  const layersToSearch = layer && layer !== "*" ? config.localeDirs.filter((d) => d.layer === layer) : config.localeDirs.filter((d) => !d.aliasOf);
2180
2253
  if (layersToSearch.length === 0) {
2181
2254
  if (layer && layer !== "*") findLayerOrThrow(config, layer);
@@ -2186,28 +2259,43 @@ async function searchTranslations(opts) {
2186
2259
  if (!found) throw new ToolError(`Locale not found: "${locale}". Available: ${config.locales.map((l) => l.code).join(", ")}. Use one of the available locale codes or file names.`, "LOCALE_NOT_FOUND");
2187
2260
  return [found];
2188
2261
  })() : config.locales;
2189
- const matches = [];
2262
+ const referenceLocale = findLocaleImpl(config, locale ?? config.defaultLocale) ?? localesToSearch[0];
2263
+ const matchedCodes = new Set((matchMode === "contains" || locale || !referenceLocale ? localesToSearch : [referenceLocale]).map((l) => l.code));
2264
+ const sheets = [];
2190
2265
  for (const localeDir of layersToSearch) for (const loc of localesToSearch) {
2191
2266
  const data = await readLocaleDataIfPresent(config, localeDir.layer, loc);
2192
2267
  if (!data) continue;
2193
- const leafKeys = getLeafKeys(data);
2194
- for (const key of leafKeys) {
2195
- const value = getNestedValue(data, key);
2196
- const valueStr = typeof value === "string" ? value : JSON.stringify(value);
2197
- const keyMatch = mode === "keys" || mode === "both" ? key.toLowerCase().includes(queryLower) : false;
2198
- const valueMatch = mode === "values" || mode === "both" ? valueStr.toLowerCase().includes(queryLower) : false;
2268
+ sheets.push({
2269
+ layer: localeDir.layer,
2270
+ locale: loc.code,
2271
+ data
2272
+ });
2273
+ }
2274
+ const matches = [];
2275
+ for (const sheet of sheets) {
2276
+ if (!matchedCodes.has(sheet.locale)) continue;
2277
+ for (const key of getLeafKeys(sheet.data)) {
2278
+ const value = getNestedValue(sheet.data, key);
2279
+ const valueStr = typeof value === "string" ? value : JSON.stringify(value) ?? "";
2280
+ const keyMatch = (mode === "keys" || mode === "both") && isMatch(key);
2281
+ const valueMatch = (mode === "values" || mode === "both") && isMatch(valueStr);
2199
2282
  if (keyMatch || valueMatch) matches.push({
2200
- layer: localeDir.layer,
2201
- locale: loc.code,
2283
+ layer: sheet.layer,
2284
+ locale: sheet.locale,
2202
2285
  key,
2203
2286
  value
2204
2287
  });
2205
2288
  }
2206
2289
  }
2207
- return {
2290
+ if (opts.includeLocales) return {
2208
2291
  matches,
2209
2292
  totalMatches: matches.length
2210
2293
  };
2294
+ const grouped = groupMatchesByKey(matches, sheets, referenceLocale?.code ?? config.defaultLocale);
2295
+ return {
2296
+ matches: grouped,
2297
+ totalMatches: grouped.length
2298
+ };
2211
2299
  }
2212
2300
  /**
2213
2301
  * Build a prefix tree of all translation keys grouped by layer and namespace.
@@ -3064,7 +3152,8 @@ function interpret(sites, ctx) {
3064
3152
  //#endregion
3065
3153
  //#region src/scanner/frontends/patterns.ts
3066
3154
  /**
3067
- * The regex path as a language frontend (#332).
3155
+ * Pattern matching as a language frontend, reached only for a file a syntax
3156
+ * frontend declined.
3068
3157
  *
3069
3158
  * Regexes frame text and report call sites; what a site means is decided once,
3070
3159
  * in the rules, the same as for every other frontend. Binding is always
@@ -3484,32 +3573,11 @@ async function extractFileEvidence(content, filePath, frontends, patterns) {
3484
3573
  };
3485
3574
  }
3486
3575
  /**
3487
- * Opt-in, via `I18N_SCANNER=ast`.
3488
- *
3489
- * The architecture is settled; the migration is not. #332 gates each frontend
3490
- * on a differential run showing it is at least as conservative as what it
3491
- * replaces, and on anny-ui the AST frontend still misses 13 keys the patterns
3492
- * find — most of them regex artifacts, a handful genuine. A key the outgoing
3493
- * frontend saw and the incoming one does not becomes an orphan, and orphans
3494
- * get deleted, so the default stays where the evidence is.
3495
- *
3496
- * Flip it once `packages/cli/scripts/scanner-diff.mjs` reports nothing in that direction.
3497
- */
3498
- let warnedRegexHatch = false;
3499
- /**
3500
- * The syntax frontends are the default (#402 for JS/TS/Vue, #405 for
3501
- * PHP/Blade); patterns read only what they decline. `I18N_SCANNER=regex`
3502
- * restores the old scanner for exactly one release — an escape hatch for
3503
- * reporting a regression, not a mode.
3576
+ * Syntax decides what a translation usage is. The pattern frontend sits last
3577
+ * and never declines, so a file no parser can read still contributes evidence
3578
+ * instead of dropping out of the scan.
3504
3579
  */
3505
3580
  function defaultFrontends(pat) {
3506
- if (process.env.I18N_SCANNER === "regex") {
3507
- if (!warnedRegexHatch) {
3508
- warnedRegexHatch = true;
3509
- log.warn("I18N_SCANNER=regex is deprecated and will be removed in the next major. If the default scanner misses something the regex found, please file it: https://github.com/fabkho/the-i18n-kit/issues");
3510
- }
3511
- return [createPatternsFrontend(pat)];
3512
- }
3513
3581
  return [...pat.bareShapes === "php" ? [phpFrontend, bladeFrontend] : [oxcFrontend], createPatternsFrontend(pat)];
3514
3582
  }
3515
3583
  const oxcFrontend = createOxcFrontend();
@@ -3667,6 +3735,7 @@ function nestedUnitIgnores(unit, units) {
3667
3735
  }
3668
3736
  async function findOrphanKeysForConfig(options) {
3669
3737
  const { keysByLayer, excludeDirs, resolveIgnorePatterns, patterns } = options;
3738
+ const declaredRegexes = buildIgnorePatternRegexes(options.declaredNamespaces ?? []);
3670
3739
  const globalScope = options.scanDirs !== void 0;
3671
3740
  const units = options.scanDirs !== void 0 ? options.scanDirs.map((d) => ({
3672
3741
  name: d,
@@ -3739,6 +3808,7 @@ async function findOrphanKeysForConfig(options) {
3739
3808
  let uncertainCount = 0;
3740
3809
  let dynamicMatchedCount = 0;
3741
3810
  let ignoredCount = 0;
3811
+ let declaredCount = 0;
3742
3812
  const misplacedUsages = [];
3743
3813
  const scanScopeByLayer = {};
3744
3814
  for (const [layerName, { keys }] of keysByLayer) {
@@ -3760,6 +3830,10 @@ async function findOrphanKeysForConfig(options) {
3760
3830
  dynamicMatchedCount++;
3761
3831
  return false;
3762
3832
  }
3833
+ if (declaredRegexes.some((re) => re.test(k))) {
3834
+ declaredCount++;
3835
+ return false;
3836
+ }
3763
3837
  if (ignoreRegexes.length > 0 && ignoreRegexes.some((re) => re.test(k))) {
3764
3838
  ignoredCount++;
3765
3839
  return false;
@@ -3809,6 +3883,7 @@ async function findOrphanKeysForConfig(options) {
3809
3883
  totalFilesDeclined,
3810
3884
  dynamicMatchedCount,
3811
3885
  ignoredCount,
3886
+ declaredCount,
3812
3887
  allDynamicKeys: allDynamicKeysRaw,
3813
3888
  dirsScanned: units.map((u) => u.dir),
3814
3889
  unresolvedKeyWarnings: unresolvedWarnings,
@@ -4785,6 +4860,7 @@ async function tally(ctx) {
4785
4860
  * referenced in source code, plus code-usage scanning.
4786
4861
  */
4787
4862
  const MISPLACED_USAGE_NOTE = "Keys referenced only from apps that do not consume their layer. Either the key belongs in a broader (shared) layer, or the usage is a bug. These keys are not counted as orphans and are never removed.";
4863
+ const DECLARED_NAMESPACE_NOTE = "Keys covered by a declaredNamespaces entry. They exist by contract rather than by a call site, so they are never reported as orphans and never removed. A declaration with no matchedKeys covers nothing in this catalog — either the namespace is gone or the pattern is wrong.";
4788
4864
  const CANDIDATE_ONLY_NOTE = "These keys are protected only by the bare-candidate net: either a dotted string somewhere merely shares their name (often a comment or a data structure), or a call too ambiguous to commit to references them (a bare t(...) that could be anything). They are not offered for removal, but dead references hide here - verify before pruning.";
4789
4865
  /** True when `child` equals `parent` or lies inside it. */
4790
4866
  function isWithin(child, parent) {
@@ -4924,6 +5000,7 @@ async function runOrphanScan(config, keysByLayer, opts) {
4924
5000
  ...opts.scanDirs?.length ? { scanDirs: opts.scanDirs } : { scanPlan: buildOrphanScanPlan(config, opts.dir) },
4925
5001
  excludeDirs: opts.excludeDirs || void 0,
4926
5002
  resolveIgnorePatterns: (layerName) => resolveOrphanIgnorePatterns(config, layerName),
5003
+ declaredNamespaces: resolveDeclaredNamespaces(config).map((d) => d.pattern),
4927
5004
  patterns: getPatternSet(config.localeFileFormat),
4928
5005
  progress
4929
5006
  });
@@ -4952,6 +5029,38 @@ function resolveOrphanIgnorePatterns(config, layer) {
4952
5029
  return layerConfig.ignorePatterns;
4953
5030
  }
4954
5031
  /**
5032
+ * The declared namespaces of a project: key patterns that exist by contract
5033
+ * rather than by a call site. They are not keyed by layer — the contract
5034
+ * defines the key set, whichever layer happens to hold it.
5035
+ */
5036
+ function resolveDeclaredNamespaces(config) {
5037
+ return config.projectConfig?.declaredNamespaces ?? [];
5038
+ }
5039
+ /**
5040
+ * Each declaration with the catalog keys it covers, so the report says which
5041
+ * keys a declaration protects and why — and, with an empty `matchedKeys`,
5042
+ * which declaration protects nothing at all.
5043
+ *
5044
+ * Coverage is read off the catalog rather than off the orphan list: a
5045
+ * namespace whose keys are also referenced in code still exists, and only a
5046
+ * declaration that matches no key anywhere is stale.
5047
+ */
5048
+ function buildDeclaredNamespaceRefs(config, keysByLayer) {
5049
+ const declarations = resolveDeclaredNamespaces(config);
5050
+ if (declarations.length === 0) return void 0;
5051
+ const allKeys = /* @__PURE__ */ new Set();
5052
+ for (const { keys } of keysByLayer.values()) for (const key of keys) allKeys.add(key);
5053
+ const sortedKeys = [...allKeys].sort(byCodePoint);
5054
+ return declarations.map(({ pattern, reason }) => {
5055
+ const [regex] = buildIgnorePatternRegexes([pattern]);
5056
+ return {
5057
+ pattern,
5058
+ reason,
5059
+ matchedKeys: regex ? sortedKeys.filter((key) => regex.test(key)) : []
5060
+ };
5061
+ });
5062
+ }
5063
+ /**
4955
5064
  * Shared helper for findOrphanKeys and removeOrphanKeys.
4956
5065
  * Resolves the locale, filters layers, validates aliases, and builds the
4957
5066
  * keysByLayer Map. Returns the resolved context — or throws on invalid input.
@@ -5029,6 +5138,7 @@ async function findOrphanKeys(opts) {
5029
5138
  sortedByLayer[keyLayer].push(key);
5030
5139
  }
5031
5140
  const misplacedCount = orphanResult.misplacedUsages.length;
5141
+ const declaredNamespaces = buildDeclaredNamespaceRefs(config, keysByLayer);
5032
5142
  return {
5033
5143
  orphanKeys: sortedByLayer,
5034
5144
  uncertainKeys: orphanResult.uncertainCount > 0 ? orphanResult.uncertainByLayer : void 0,
@@ -5036,6 +5146,8 @@ async function findOrphanKeys(opts) {
5036
5146
  candidateOnlyNote: orphanResult.candidateOnlyCount > 0 ? CANDIDATE_ONLY_NOTE : void 0,
5037
5147
  misplacedUsages: misplacedCount > 0 ? orphanResult.misplacedUsages : void 0,
5038
5148
  misplacedUsageNote: misplacedCount > 0 ? MISPLACED_USAGE_NOTE : void 0,
5149
+ declaredNamespaces,
5150
+ declaredNamespaceNote: declaredNamespaces ? DECLARED_NAMESPACE_NOTE : void 0,
5039
5151
  summary: {
5040
5152
  totalKeys,
5041
5153
  orphanCount: orphanResult.orphanCount,
@@ -5044,6 +5156,7 @@ async function findOrphanKeys(opts) {
5044
5156
  misplacedCount,
5045
5157
  dynamicMatchedCount: orphanResult.dynamicMatchedCount,
5046
5158
  ignoredCount: orphanResult.ignoredCount,
5159
+ declaredCount: orphanResult.declaredCount,
5047
5160
  usedCount: totalKeys - orphanResult.orphanCount - orphanResult.uncertainCount - misplacedCount,
5048
5161
  filesScanned: orphanResult.totalFilesScanned,
5049
5162
  filesDeclined: orphanResult.totalFilesDeclined,
@@ -5145,23 +5258,29 @@ async function removeOrphanKeys(opts) {
5145
5258
  const totalFilesScanned = orphanResult.totalFilesScanned;
5146
5259
  const dynamicMatchedCount = orphanResult.dynamicMatchedCount;
5147
5260
  const ignoredCount = orphanResult.ignoredCount;
5261
+ const declaredCount = orphanResult.declaredCount;
5148
5262
  const misplacedCount = orphanResult.misplacedUsages.length;
5149
5263
  const misplacedUsages = misplacedCount > 0 ? orphanResult.misplacedUsages : void 0;
5150
5264
  const misplacedUsageNote = misplacedCount > 0 ? MISPLACED_USAGE_NOTE : void 0;
5265
+ const declaredNamespaces = buildDeclaredNamespaceRefs(config, keysByLayer);
5266
+ const declaredNamespaceNote = declaredNamespaces ? DECLARED_NAMESPACE_NOTE : void 0;
5151
5267
  const scanScope = relativeScanScope(orphanResult, dir);
5152
5268
  const allDynamicKeys = toDynamicKeyEntries(orphanResult.allDynamicKeys, dir);
5153
5269
  if (orphanCount === 0) {
5154
5270
  const messageParts = ["No orphan keys found."];
5155
5271
  if (dynamicMatchedCount > 0) messageParts.push(`${dynamicMatchedCount} key(s) were excluded by dynamic pattern matching.`);
5156
5272
  if (ignoredCount > 0) messageParts.push(`${ignoredCount} key(s) were excluded by ignore patterns.`);
5273
+ if (declaredCount > 0) messageParts.push(`${declaredCount} key(s) were excluded by declared namespaces (see declaredNamespaces).`);
5157
5274
  if (orphanResult.uncertainCount > 0) messageParts.push(`${orphanResult.uncertainCount} uncertain key(s) were excluded because they overlap with dynamic translation patterns.`);
5158
5275
  if (misplacedCount > 0) messageParts.push(`${misplacedCount} key(s) are referenced only outside their layer's scope (see misplacedUsages).`);
5159
- if (dynamicMatchedCount === 0 && ignoredCount === 0 && orphanResult.uncertainCount === 0 && misplacedCount === 0) messageParts.push("All translation keys are referenced in code.");
5276
+ if (dynamicMatchedCount === 0 && ignoredCount === 0 && declaredCount === 0 && orphanResult.uncertainCount === 0 && misplacedCount === 0) messageParts.push("All translation keys are referenced in code.");
5160
5277
  return {
5161
5278
  orphanKeys: {},
5162
5279
  uncertainKeys: orphanResult.uncertainCount > 0 ? orphanResult.uncertainByLayer : void 0,
5163
5280
  misplacedUsages,
5164
5281
  misplacedUsageNote,
5282
+ declaredNamespaces,
5283
+ declaredNamespaceNote,
5165
5284
  summary: {
5166
5285
  totalKeys,
5167
5286
  orphanCount: 0,
@@ -5169,6 +5288,7 @@ async function removeOrphanKeys(opts) {
5169
5288
  misplacedCount,
5170
5289
  dynamicMatchedCount,
5171
5290
  ignoredCount,
5291
+ declaredCount,
5172
5292
  filesScanned: totalFilesScanned,
5173
5293
  scanScope,
5174
5294
  message: messageParts.join(" ")
@@ -5181,6 +5301,8 @@ async function removeOrphanKeys(opts) {
5181
5301
  uncertainKeys: orphanResult.uncertainCount > 0 ? orphanResult.uncertainByLayer : void 0,
5182
5302
  misplacedUsages,
5183
5303
  misplacedUsageNote,
5304
+ declaredNamespaces,
5305
+ declaredNamespaceNote,
5184
5306
  summary: {
5185
5307
  dryRun: true,
5186
5308
  totalKeys,
@@ -5189,10 +5311,11 @@ async function removeOrphanKeys(opts) {
5189
5311
  misplacedCount,
5190
5312
  dynamicMatchedCount,
5191
5313
  ignoredCount,
5314
+ declaredCount,
5192
5315
  usedCount: totalKeys - orphanCount - orphanResult.uncertainCount - misplacedCount,
5193
5316
  filesScanned: totalFilesScanned,
5194
5317
  scanScope,
5195
- message: `Found ${orphanCount} orphan key(s) safe to remove.${orphanResult.uncertainCount > 0 ? ` ${orphanResult.uncertainCount} uncertain key(s) excluded (overlap with dynamic translation patterns).` : ""}${misplacedCount > 0 ? ` ${misplacedCount} key(s) referenced only outside their layer's scope were excluded (see misplacedUsages).` : ""} ${dynamicMatchedCount > 0 ? `${dynamicMatchedCount} key(s) matched dynamic patterns and were excluded. ` : ""}${ignoredCount > 0 ? `${ignoredCount} key(s) matched ignore patterns and were excluded. ` : ""}Call again with dryRun: false to remove them.`
5318
+ message: `Found ${orphanCount} orphan key(s) safe to remove.${orphanResult.uncertainCount > 0 ? ` ${orphanResult.uncertainCount} uncertain key(s) excluded (overlap with dynamic translation patterns).` : ""}${misplacedCount > 0 ? ` ${misplacedCount} key(s) referenced only outside their layer's scope were excluded (see misplacedUsages).` : ""} ${dynamicMatchedCount > 0 ? `${dynamicMatchedCount} key(s) matched dynamic patterns and were excluded. ` : ""}${ignoredCount > 0 ? `${ignoredCount} key(s) matched ignore patterns and were excluded. ` : ""}${declaredCount > 0 ? `${declaredCount} key(s) are covered by declared namespaces and were excluded. ` : ""}Call again with dryRun: false to remove them.`
5196
5319
  }
5197
5320
  };
5198
5321
  if (allDynamicKeys.length > 0) {
@@ -5227,6 +5350,8 @@ async function removeOrphanKeys(opts) {
5227
5350
  uncertainKeys: orphanResult.uncertainCount > 0 ? orphanResult.uncertainByLayer : void 0,
5228
5351
  misplacedUsages,
5229
5352
  misplacedUsageNote,
5353
+ declaredNamespaces,
5354
+ declaredNamespaceNote,
5230
5355
  summary: {
5231
5356
  dryRun: false,
5232
5357
  totalKeys,
@@ -5235,6 +5360,7 @@ async function removeOrphanKeys(opts) {
5235
5360
  misplacedCount,
5236
5361
  dynamicMatchedCount,
5237
5362
  ignoredCount,
5363
+ declaredCount,
5238
5364
  remainingCount: totalKeys - orphanCount,
5239
5365
  filesWritten: totalFilesWritten,
5240
5366
  filesScanned: totalFilesScanned,
@@ -5545,14 +5671,19 @@ function groupBy(items, keyOf) {
5545
5671
  }
5546
5672
  /**
5547
5673
  * Classify one unit's static keys: keys not resolvable in the searched
5548
- * layers become undefined findings — unless an ignore pattern excludes
5549
- * them, a dynamic pattern overlaps them (possible partial extraction of a
5550
- * dynamic expression), or they are only probed via $te.
5674
+ * layers become undefined findings — unless a declared namespace covers
5675
+ * them, an ignore pattern excludes them, a dynamic pattern overlaps them
5676
+ * (possible partial extraction of a dynamic expression), or they are only
5677
+ * probed via $te.
5551
5678
  */
5552
5679
  function classifyStaticKeys(usages, dynRegexes, ctx, outcome) {
5553
5680
  for (const [key, keyUsages] of groupBy(usages, (u) => u.key)) {
5554
5681
  if (key.endsWith(".")) continue;
5555
5682
  if (ctx.resolvable.has(key)) continue;
5683
+ if (ctx.declaredRegexes.some((re) => re.test(key))) {
5684
+ outcome.declaredCount++;
5685
+ continue;
5686
+ }
5556
5687
  if (ctx.ignoreRegexes.some((re) => re.test(key))) {
5557
5688
  outcome.ignoredCount++;
5558
5689
  continue;
@@ -5608,6 +5739,7 @@ function classifyUnitUsages(scan, ctx) {
5608
5739
  undefinedKeys: [],
5609
5740
  uncertainKeys: [],
5610
5741
  ignoredCount: 0,
5742
+ declaredCount: 0,
5611
5743
  checkedKeys: new Set(scan.uniqueKeys)
5612
5744
  };
5613
5745
  const dynRegexes = buildDynamicKeyRegexes([...scan.dynamicKeys, ...[...scan.bareDynamicCandidates].map((expression) => ({ expression }))]);
@@ -5725,6 +5857,8 @@ async function checkUndefinedKeys(opts = {}) {
5725
5857
  let filesScanned = 0;
5726
5858
  let filesDeclined = 0;
5727
5859
  let ignoredCount = 0;
5860
+ let declaredCount = 0;
5861
+ const declaredRegexes = buildIgnorePatternRegexes(resolveDeclaredNamespaces(config).map((declaration) => declaration.pattern));
5728
5862
  for (const unit of units) {
5729
5863
  const ignores = globalScope ? [] : nestedUnitIgnores(unit, units);
5730
5864
  const scan = await scanSourceFiles(unit.dir, [...opts.excludeDirs ?? [], ...ignores], patterns);
@@ -5737,11 +5871,13 @@ async function checkUndefinedKeys(opts = {}) {
5737
5871
  searchedLayers,
5738
5872
  resolvable: resolvablePaths(searchedLayers),
5739
5873
  ignoreRegexes: buildIgnorePatternRegexes(searchedLayers.flatMap((layer) => resolveOrphanIgnorePatterns(config, layer) ?? [])),
5874
+ declaredRegexes,
5740
5875
  projectDir: dir
5741
5876
  });
5742
5877
  undefinedKeys.push(...outcome.undefinedKeys);
5743
5878
  uncertainKeys.push(...outcome.uncertainKeys);
5744
5879
  ignoredCount += outcome.ignoredCount;
5880
+ declaredCount += outcome.declaredCount;
5745
5881
  for (const key of outcome.checkedKeys) checkedKeys.add(key);
5746
5882
  }
5747
5883
  undefinedKeys.sort(byAppThenKey);
@@ -5755,6 +5891,7 @@ async function checkUndefinedKeys(opts = {}) {
5755
5891
  undefinedCount: undefinedKeys.length,
5756
5892
  uncertainCount: uncertainKeys.length,
5757
5893
  ignoredCount,
5894
+ declaredCount,
5758
5895
  filesScanned,
5759
5896
  filesDeclined,
5760
5897
  locale: localeCode,
@@ -5770,6 +5907,6 @@ async function checkUndefinedKeys(opts = {}) {
5770
5907
  });
5771
5908
  }
5772
5909
  //#endregion
5773
- export { translateKey as A, findEmptyTranslations as C, listNamespaces as D, listLocaleDirs as E, buildTranslationSystemPrompt as F, buildTranslationUserMessage as I, validatePlaceholders as L, buildLayerGraph as M, serializeLayerGraph as N, searchTranslations as O, extractJsonFromResponse as P, resolveProtectedLocales as R, detectConfig as S, getTranslations as T, createBladeFrontend as _, scanCodeUsage as a, createOxcFrontend as b, moveTranslationKey as c, scaffoldLocaleFiles as d, writeTranslations as f, createPatternsFrontend as g, getPatternSet as h, removeOrphanKeys as i, translateMissing as j, computeProgressTotal as k, removeTranslations as l, VUE_NUXT_PATTERNS as m, findDuplicateKeys as n, getTranslationStatus as o, scanSourceFiles as p, findOrphanKeys as r, initProjectConfig as s, checkUndefinedKeys as t, renameTranslationKey as u, LARAVEL_PATTERNS as v, getMissingTranslations as w, describeProject as x, createPhpFrontend as y, readLocaleData as z };
5910
+ export { extractJsonFromResponse as A, listNamespaces as C, translateMissing as D, translateKey as E, readLocaleData as F, buildTranslationUserMessage as M, validatePlaceholders as N, buildLayerGraph as O, resolveProtectedLocales as P, listLocaleDirs as S, computeProgressTotal as T, describeProject as _, scanCodeUsage as a, getMissingTranslations as b, moveTranslationKey as c, scaffoldLocaleFiles as d, writeTranslations as f, createPhpFrontend as g, LARAVEL_PATTERNS as h, removeOrphanKeys as i, buildTranslationSystemPrompt as j, serializeLayerGraph as k, removeTranslations as l, createPatternsFrontend as m, findDuplicateKeys as n, getTranslationStatus as o, scanSourceFiles as p, findOrphanKeys as r, initProjectConfig as s, checkUndefinedKeys as t, renameTranslationKey as u, detectConfig as v, searchTranslations as w, getTranslations as x, findEmptyTranslations as y };
5774
5911
 
5775
- //# sourceMappingURL=operations-CNqEBD__.js.map
5912
+ //# sourceMappingURL=operations-Daf2Ommm.js.map