next-tongue 0.9.3 → 0.9.4

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.
@@ -12,12 +12,15 @@ function store() {
12
12
  escapes: new Map(),
13
13
  rendered: [],
14
14
  listeners: new Set(),
15
+ snapshot: null,
15
16
  };
16
17
  }
17
18
  return g.__nextTongueDev;
18
19
  }
19
20
  function notify() {
20
- for (const l of store().listeners)
21
+ const s = store();
22
+ s.snapshot = null;
23
+ for (const l of s.listeners)
21
24
  l();
22
25
  }
23
26
  export function tongueDevLoaded(namespace, key) {
@@ -74,13 +77,16 @@ export function subscribeTongueDev(listener) {
74
77
  }
75
78
  export function getTongueDevSnapshot() {
76
79
  const s = store();
77
- return {
80
+ if (s.snapshot)
81
+ return s.snapshot;
82
+ s.snapshot = {
78
83
  loaded: [...s.loaded.values()],
79
84
  missing: [...s.missing.values()],
80
85
  fallbacks: [...s.fallbacks.values()],
81
86
  escapes: [...s.escapes.values()],
82
87
  rendered: [...s.rendered],
83
88
  };
89
+ return s.snapshot;
84
90
  }
85
91
  /** @internal test helper */
86
92
  export function __resetTongueDev() {
@@ -68,8 +68,11 @@ export function useFormat() {
68
68
  export function useT(namespace) {
69
69
  const ns = (namespace ?? '_root');
70
70
  const { locale, loadNamespace } = useTongue();
71
+ // Literal `t('key')` is rewritten by the call-site loader to per-key imports;
72
+ // `useT()` itself must still be safe during SSR without loadNamespace.
73
+ // Invoking the returned `t` without a rewrite (dynamic/escaped) throws below.
71
74
  if (!loadNamespace) {
72
- throw new TongueError('TONGUE_E_NO_PROVIDER', `useT('${String(ns)}') needs the call-site loader (or pass loadNamespace to TongueProvider). Prefer translating on the server with getT and passing strings.`);
75
+ return createLoaderStubT(locale, String(ns));
73
76
  }
74
77
  const messages = loadNamespace(ns);
75
78
  return createT({
@@ -78,6 +81,24 @@ export function useT(namespace) {
78
81
  loadNamespace,
79
82
  });
80
83
  }
84
+ /**
85
+ * Stub used when the call-site loader owns message delivery. Calling the
86
+ * function (or .rich / .dynamic / .ns) means a key was not rewritten — usually
87
+ * a missing loader or a non-literal key without `t.dynamic({ from })`.
88
+ */
89
+ function createLoaderStubT(locale, ns) {
90
+ const fail = () => {
91
+ throw new TongueError('TONGUE_E_NO_PROVIDER', `useT('${ns}') call was not rewritten to a per-key import. Ensure withTongue() is applied, or pass loadNamespace to TongueProvider. Prefer getT on the server for static copy; for dynamic keys use t.dynamic(key, { from: [...] }).`);
92
+ };
93
+ const t = Object.assign((..._args) => fail(), {
94
+ rich: fail,
95
+ has: () => false,
96
+ dynamic: fail,
97
+ ns: fail,
98
+ });
99
+ void locale;
100
+ return t;
101
+ }
81
102
  export function getClientLocales() {
82
103
  return clientLocales;
83
104
  }
@@ -5,9 +5,18 @@ export type ReferencedKey = {
5
5
  file: string;
6
6
  line: number;
7
7
  };
8
+ export type ScanResult = {
9
+ refs: ReferencedKey[];
10
+ /**
11
+ * Namespaces where `t(variable)` / `t.ns(...)` was seen — unused-key warnings
12
+ * for these namespaces are suppressed (dynamic / whole-ns usage).
13
+ */
14
+ dynamicNamespaces: Set<string>;
15
+ };
8
16
  /**
9
17
  * Find `t('key')` / `t.rich('key')` under a known `useT`/`getT` namespace,
10
- * plus bare `useT('ns')` / `getT('ns')` for namespace awareness.
18
+ * plus `t.dynamic(..., { from })` declared keys and dynamic `t(var)` markers.
11
19
  */
12
20
  export declare function scanReferencedKeys(cwd: string, roots?: string[]): Promise<ReferencedKey[]>;
13
- export declare function extractFromSource(source: string, file: string): ReferencedKey[];
21
+ export declare function scanKeys(cwd: string, roots?: string[]): Promise<ScanResult>;
22
+ export declare function extractFromSource(source: string, file: string): ScanResult;
@@ -12,9 +12,12 @@ const SKIP_DIRS = new Set([
12
12
  ]);
13
13
  /**
14
14
  * Find `t('key')` / `t.rich('key')` under a known `useT`/`getT` namespace,
15
- * plus bare `useT('ns')` / `getT('ns')` for namespace awareness.
15
+ * plus `t.dynamic(..., { from })` declared keys and dynamic `t(var)` markers.
16
16
  */
17
17
  export async function scanReferencedKeys(cwd, roots = ['app', 'src', 'components']) {
18
+ return (await scanKeys(cwd, roots)).refs;
19
+ }
20
+ export async function scanKeys(cwd, roots = ['app', 'src', 'components']) {
18
21
  const files = [];
19
22
  for (const root of roots) {
20
23
  const abs = join(cwd, root);
@@ -27,13 +30,17 @@ export async function scanReferencedKeys(cwd, roots = ['app', 'src', 'components
27
30
  }
28
31
  await walk(abs, files);
29
32
  }
30
- const out = [];
33
+ const refs = [];
34
+ const dynamicNamespaces = new Set();
31
35
  for (const file of files) {
32
36
  const src = await readFile(file, 'utf8');
33
37
  const rel = relative(cwd, file);
34
- out.push(...extractFromSource(src, rel));
38
+ const part = extractFromSource(src, rel);
39
+ refs.push(...part.refs);
40
+ for (const ns of part.dynamicNamespaces)
41
+ dynamicNamespaces.add(ns);
35
42
  }
36
- return out;
43
+ return { refs, dynamicNamespaces };
37
44
  }
38
45
  async function walk(dir, out) {
39
46
  const entries = await readdir(dir, { withFileTypes: true });
@@ -57,6 +64,7 @@ function extname(name) {
57
64
  }
58
65
  export function extractFromSource(source, file) {
59
66
  const out = [];
67
+ const dynamicNamespaces = new Set();
60
68
  const translators = new Map(); // var → ns
61
69
  // useT('ns') | getT('ns') | getT(locale, 'ns')
62
70
  const declRe = /(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?(?:useT|getT)\s*\(\s*(?:[^'"\n,]+,\s*)?(['"])([^'"]*)\2\s*\)/g;
@@ -64,23 +72,65 @@ export function extractFromSource(source, file) {
64
72
  while ((m = declRe.exec(source)) !== null) {
65
73
  translators.set(m[1], m[3] || '_root');
66
74
  }
75
+ const push = (ns, key, index) => {
76
+ const fullKey = ns === '_root' || ns === '' ? key : `${ns}.${key}`;
77
+ out.push({
78
+ fullKey,
79
+ namespace: ns || '_root',
80
+ key,
81
+ file,
82
+ line: lineAt(source, index),
83
+ });
84
+ };
67
85
  for (const [varName, ns] of translators) {
86
+ // Literal t('key') / t.rich('key')
68
87
  const callRe = new RegExp(`\\b${escapeRegExp(varName)}(?:\\.rich)?\\s*\\(\\s*(['"])([^'"]+)\\1`, 'g');
69
88
  let cm;
70
89
  while ((cm = callRe.exec(source)) !== null) {
71
- const key = cm[2];
72
- const fullKey = ns === '_root' || ns === '' ? key : `${ns}.${key}`;
73
- const line = lineAt(source, cm.index);
74
- out.push({
75
- fullKey,
76
- namespace: ns || '_root',
77
- key,
78
- file,
79
- line,
80
- });
90
+ push(ns, cm[2], cm.index);
91
+ }
92
+ // t.dynamic(any, { from: ['a', 'b'] })
93
+ const dynRe = new RegExp(`\\b${escapeRegExp(varName)}\\.dynamic\\s*\\(`, 'g');
94
+ let dm;
95
+ while ((dm = dynRe.exec(source)) !== null) {
96
+ const tail = source.slice(dm.index, dm.index + 400);
97
+ const fromMatch = /from\s*:\s*\[([^\]]*)\]/.exec(tail);
98
+ if (fromMatch) {
99
+ const keys = fromMatch[1].match(/['"]([^'"]+)['"]/g) ?? [];
100
+ for (const raw of keys) {
101
+ const key = raw.slice(1, -1);
102
+ push(ns, key, dm.index);
103
+ }
104
+ }
105
+ dynamicNamespaces.add(ns || '_root');
106
+ }
107
+ // t.ns('other') — whole namespace marked dynamic
108
+ const nsRe = new RegExp(`\\b${escapeRegExp(varName)}\\.ns\\s*\\(\\s*(['"])([^'"]+)\\1`, 'g');
109
+ let nm;
110
+ while ((nm = nsRe.exec(source)) !== null) {
111
+ dynamicNamespaces.add(nm[2] || '_root');
112
+ }
113
+ // Non-literal t(var) / t.rich(var) — mark namespace dynamic + harvest nearby string arrays
114
+ const nonLitRe = new RegExp(`\\b${escapeRegExp(varName)}(?:\\.rich)?\\s*\\(\\s*(?!['"])([A-Za-z_$][\\w$]*)`, 'g');
115
+ let nl;
116
+ let sawNonLit = false;
117
+ while ((nl = nonLitRe.exec(source)) !== null) {
118
+ sawNonLit = true;
119
+ dynamicNamespaces.add(ns || '_root');
120
+ }
121
+ if (sawNonLit) {
122
+ // Keys listed in string arrays in this file (e.g. KEYS.map((k) => t(k)))
123
+ const arrayLitRe = /\[\s*((?:['"][^'"]+['"]\s*,?\s*)+)\]/g;
124
+ let am;
125
+ while ((am = arrayLitRe.exec(source)) !== null) {
126
+ const keys = am[1].match(/['"]([^'"]+)['"]/g) ?? [];
127
+ for (const raw of keys) {
128
+ push(ns, raw.slice(1, -1), am.index);
129
+ }
130
+ }
81
131
  }
82
132
  }
83
- return out;
133
+ return { refs: out, dynamicNamespaces };
84
134
  }
85
135
  function lineAt(source, index) {
86
136
  let line = 1;
@@ -1,5 +1,5 @@
1
1
  import { loadLocaleMessages } from '../load-messages.js';
2
- import { scanReferencedKeys } from '../scan-keys.js';
2
+ import { scanKeys } from '../scan-keys.js';
3
3
  function severityFor(level, alwaysError = false) {
4
4
  if (alwaysError)
5
5
  return 'error';
@@ -7,6 +7,10 @@ function severityFor(level, alwaysError = false) {
7
7
  return null;
8
8
  return level === 'error' ? 'error' : 'warn';
9
9
  }
10
+ function namespaceOf(fullKey) {
11
+ const i = fullKey.indexOf('.');
12
+ return i === -1 ? '_root' : fullKey.slice(0, i);
13
+ }
10
14
  /** unused + undefined checks against scanned source references. */
11
15
  export async function validateUsage(config, cwd) {
12
16
  const issues = [];
@@ -20,7 +24,7 @@ export async function validateUsage(config, cwd) {
20
24
  defined.add(ns === '_root' ? key : `${ns}.${key}`);
21
25
  }
22
26
  }
23
- const refs = await scanReferencedKeys(cwd);
27
+ const { refs, dynamicNamespaces } = await scanKeys(cwd);
24
28
  const used = new Set();
25
29
  for (const ref of refs) {
26
30
  used.add(ref.fullKey);
@@ -40,6 +44,9 @@ export async function validateUsage(config, cwd) {
40
44
  for (const fullKey of defined) {
41
45
  if (used.has(fullKey))
42
46
  continue;
47
+ // Dynamic t(var) / t.ns / t.dynamic — do not false-flag catalog keys as unused
48
+ if (dynamicNamespaces.has(namespaceOf(fullKey)))
49
+ continue;
43
50
  issues.push({
44
51
  check: 'unused',
45
52
  severity: unusedSev,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-tongue",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "Compiler-first, type-safe i18n for the Next.js 16 App Router",
5
5
  "license": "MIT",
6
6
  "type": "module",