next-tongue 0.9.12 → 0.9.13

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.
@@ -4,6 +4,7 @@ import { loadConfig } from '../config/load-config.js';
4
4
  import { buildMessages } from '../compiler/build.js';
5
5
  import { loadLocaleMessages, resolveMessagePath } from '../compiler/load-messages.js';
6
6
  import { DEFAULT_SCAN_ROOTS, scanReferencedKeys, } from '../compiler/scan-keys.js';
7
+ import { collectUnusedIgnores, isUnusedIgnored, matchesUnusedIgnoreGlob, } from '../compiler/unused-meta.js';
7
8
  import { TongueError } from '../errors.js';
8
9
  export async function cmdExtract(opts) {
9
10
  const { config, cwd } = await loadConfig({
@@ -28,8 +29,6 @@ export async function cmdExtract(opts) {
28
29
  defined.add(ns === '_root' ? key : `${ns}.${key}`);
29
30
  }
30
31
  }
31
- const missing = [...used].filter((k) => !defined.has(k)).sort();
32
- const unused = [...defined].filter((k) => !used.has(k)).sort();
33
32
  const placeholder = opts.placeholder ?? null; // null → use key as value
34
33
  const abs = resolveMessagePath(cwd, config.messages, config.defaultLocale);
35
34
  let raw;
@@ -46,6 +45,14 @@ export async function cmdExtract(opts) {
46
45
  catch (err) {
47
46
  throw new TongueError('TONGUE_E_PARSE', `Invalid JSON: ${err instanceof Error ? err.message : err}`, { file: abs });
48
47
  }
48
+ const metaIgnores = collectUnusedIgnores(json);
49
+ const configIgnores = config.strict.unusedIgnore;
50
+ const missing = [...used].filter((k) => !defined.has(k)).sort();
51
+ const unused = [...defined]
52
+ .filter((k) => !used.has(k))
53
+ .filter((k) => !isUnusedIgnored(k, metaIgnores))
54
+ .filter((k) => !matchesUnusedIgnoreGlob(k, configIgnores))
55
+ .sort();
49
56
  let added = 0;
50
57
  let pruned = 0;
51
58
  for (const fullKey of missing) {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Catalog `$meta` flags that suppress `unused` (and extract `--prune`).
3
+ *
4
+ * Per key (sibling `key$meta`):
5
+ * { "planned": true }
6
+ * { "ignoreUnused": true }
7
+ * { "@tongue-ignore": true }
8
+ *
9
+ * Per namespace (child `$meta` on the namespace object):
10
+ * "common": { "$meta": { "planned": true }, "seeAll": "…" }
11
+ */
12
+ export type Nested = Record<string, unknown>;
13
+ export type UnusedIgnoreIndex = {
14
+ /** Exact full keys (e.g. `common.seeAll`). */
15
+ keys: Set<string>;
16
+ /** Namespace prefixes whose unused keys are ignored (`common` → `common.*`). */
17
+ namespaces: Set<string>;
18
+ };
19
+ /** Walk default-locale JSON and collect planned / @tongue-ignore markers. */
20
+ export declare function collectUnusedIgnores(root: Nested): UnusedIgnoreIndex;
21
+ export declare function isUnusedIgnored(fullKey: string, index: UnusedIgnoreIndex): boolean;
22
+ /** Glob-lite for `strict.unusedIgnore` (`common.*`, `*.seeAll`). */
23
+ export declare function matchesUnusedIgnoreGlob(fullKey: string, patterns: readonly string[]): boolean;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Catalog `$meta` flags that suppress `unused` (and extract `--prune`).
3
+ *
4
+ * Per key (sibling `key$meta`):
5
+ * { "planned": true }
6
+ * { "ignoreUnused": true }
7
+ * { "@tongue-ignore": true }
8
+ *
9
+ * Per namespace (child `$meta` on the namespace object):
10
+ * "common": { "$meta": { "planned": true }, "seeAll": "…" }
11
+ */
12
+ function metaIgnoresUnused(meta) {
13
+ if (!meta || typeof meta !== 'object' || Array.isArray(meta))
14
+ return false;
15
+ const m = meta;
16
+ if (m.planned === true)
17
+ return true;
18
+ if (m.ignoreUnused === true)
19
+ return true;
20
+ if (m['@tongue-ignore'] === true)
21
+ return true;
22
+ // Allow string form: "@tongue-ignore" or "planned"
23
+ if (typeof m.unused === 'string') {
24
+ const u = m.unused.toLowerCase();
25
+ if (u === 'ignore' || u === 'planned' || u === '@tongue-ignore')
26
+ return true;
27
+ }
28
+ return false;
29
+ }
30
+ /** Walk default-locale JSON and collect planned / @tongue-ignore markers. */
31
+ export function collectUnusedIgnores(root) {
32
+ const keys = new Set();
33
+ const namespaces = new Set();
34
+ walk(root, '', keys, namespaces);
35
+ return { keys, namespaces };
36
+ }
37
+ function walk(node, prefix, keys, namespaces) {
38
+ const nsMeta = node['$meta'];
39
+ if (prefix && metaIgnoresUnused(nsMeta)) {
40
+ namespaces.add(prefix);
41
+ }
42
+ for (const [key, value] of Object.entries(node)) {
43
+ if (key === '$meta' || key.endsWith('$meta') || key.endsWith('$description')) {
44
+ continue;
45
+ }
46
+ const full = prefix ? `${prefix}.${key}` : key;
47
+ if (value != null && typeof value === 'object' && !Array.isArray(value)) {
48
+ walk(value, full, keys, namespaces);
49
+ continue;
50
+ }
51
+ if (typeof value !== 'string')
52
+ continue;
53
+ const meta = node[`${key}$meta`];
54
+ if (metaIgnoresUnused(meta)) {
55
+ keys.add(full);
56
+ }
57
+ }
58
+ }
59
+ export function isUnusedIgnored(fullKey, index) {
60
+ if (index.keys.has(fullKey))
61
+ return true;
62
+ for (const ns of index.namespaces) {
63
+ if (fullKey === ns || fullKey.startsWith(`${ns}.`))
64
+ return true;
65
+ }
66
+ return false;
67
+ }
68
+ /** Glob-lite for `strict.unusedIgnore` (`common.*`, `*.seeAll`). */
69
+ export function matchesUnusedIgnoreGlob(fullKey, patterns) {
70
+ for (const raw of patterns) {
71
+ const p = raw.trim();
72
+ if (!p)
73
+ continue;
74
+ if (p === fullKey)
75
+ return true;
76
+ if (!p.includes('*'))
77
+ continue;
78
+ const re = new RegExp(`^${p
79
+ .split('*')
80
+ .map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
81
+ .join('.*')}$`);
82
+ if (re.test(fullKey))
83
+ return true;
84
+ }
85
+ return false;
86
+ }
@@ -1,5 +1,7 @@
1
- import { loadLocaleMessages } from '../load-messages.js';
1
+ import { readFile } from 'node:fs/promises';
2
+ import { loadLocaleMessages, resolveMessagePath, } from '../load-messages.js';
2
3
  import { DEFAULT_SCAN_ROOTS, scanKeys } from '../scan-keys.js';
4
+ import { collectUnusedIgnores, isUnusedIgnored, matchesUnusedIgnoreGlob, } from '../unused-meta.js';
3
5
  function severityFor(level, alwaysError = false) {
4
6
  if (alwaysError)
5
7
  return 'error';
@@ -11,25 +13,6 @@ function namespaceOf(fullKey) {
11
13
  const i = fullKey.indexOf('.');
12
14
  return i === -1 ? '_root' : fullKey.slice(0, i);
13
15
  }
14
- /** Glob-lite: `*` matches any substring (e.g. `common.*`, `*.seeAll`). */
15
- function matchesIgnore(fullKey, patterns) {
16
- for (const raw of patterns) {
17
- const p = raw.trim();
18
- if (!p)
19
- continue;
20
- if (p === fullKey)
21
- return true;
22
- if (!p.includes('*'))
23
- continue;
24
- const re = new RegExp(`^${p
25
- .split('*')
26
- .map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
27
- .join('.*')}$`);
28
- if (re.test(fullKey))
29
- return true;
30
- }
31
- return false;
32
- }
33
16
  /** unused + undefined + undeclared client dynamic checks against scanned source. */
34
17
  export async function validateUsage(config, cwd) {
35
18
  const issues = [];
@@ -71,10 +54,13 @@ export async function validateUsage(config, cwd) {
71
54
  const unusedSev = severityFor(config.strict.unused);
72
55
  if (unusedSev) {
73
56
  const ignore = config.strict.unusedIgnore;
57
+ const metaIgnores = await loadUnusedMetaIgnores(config, cwd);
74
58
  for (const fullKey of defined) {
75
59
  if (used.has(fullKey))
76
60
  continue;
77
- if (matchesIgnore(fullKey, ignore))
61
+ if (matchesUnusedIgnoreGlob(fullKey, ignore))
62
+ continue;
63
+ if (isUnusedIgnored(fullKey, metaIgnores))
78
64
  continue;
79
65
  // Dynamic t(var) / t.ns / t.dynamic — do not false-flag catalog keys as unused
80
66
  if (dynamicNamespaces.has(namespaceOf(fullKey)))
@@ -90,3 +76,14 @@ export async function validateUsage(config, cwd) {
90
76
  }
91
77
  return issues;
92
78
  }
79
+ async function loadUnusedMetaIgnores(config, cwd) {
80
+ const abs = resolveMessagePath(cwd, config.messages, config.defaultLocale);
81
+ try {
82
+ const raw = await readFile(abs, 'utf8');
83
+ const json = JSON.parse(raw);
84
+ return collectUnusedIgnores(json);
85
+ }
86
+ catch {
87
+ return collectUnusedIgnores({});
88
+ }
89
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-tongue",
3
- "version": "0.9.12",
3
+ "version": "0.9.13",
4
4
  "description": "Compiler-first, type-safe i18n for the Next.js 16 App Router",
5
5
  "license": "MIT",
6
6
  "type": "module",