next-tongue 0.9.4 → 0.9.5

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.
@@ -13,15 +13,28 @@ function store() {
13
13
  rendered: [],
14
14
  listeners: new Set(),
15
15
  snapshot: null,
16
+ notifyScheduled: false,
16
17
  };
17
18
  }
18
19
  return g.__nextTongueDev;
19
20
  }
21
+ /**
22
+ * Invalidate snapshot and notify subscribers **after** the current render.
23
+ * Synchronous notify from inside `t()` (render) caused
24
+ * "Cannot update DevOverlay while rendering Header".
25
+ */
20
26
  function notify() {
21
27
  const s = store();
22
28
  s.snapshot = null;
23
- for (const l of s.listeners)
24
- l();
29
+ if (s.notifyScheduled)
30
+ return;
31
+ s.notifyScheduled = true;
32
+ queueMicrotask(() => {
33
+ const st = store();
34
+ st.notifyScheduled = false;
35
+ for (const l of st.listeners)
36
+ l();
37
+ });
25
38
  }
26
39
  export function tongueDevLoaded(namespace, key) {
27
40
  const fullKey = namespace === '_root' || namespace === '' ? key : `${namespace}.${key}`;
@@ -8,8 +8,8 @@ export type ReferencedKey = {
8
8
  export type ScanResult = {
9
9
  refs: ReferencedKey[];
10
10
  /**
11
- * Namespaces where `t(variable)` / `t.ns(...)` was seen — unused-key warnings
12
- * for these namespaces are suppressed (dynamic / whole-ns usage).
11
+ * Namespaces where `t(variable)` / `t.ns(...)` / `t.dynamic` was seen —
12
+ * unused-key warnings for these namespaces are suppressed.
13
13
  */
14
14
  dynamicNamespaces: Set<string>;
15
15
  };
@@ -19,4 +19,6 @@ export type ScanResult = {
19
19
  */
20
20
  export declare function scanReferencedKeys(cwd: string, roots?: string[]): Promise<ReferencedKey[]>;
21
21
  export declare function scanKeys(cwd: string, roots?: string[]): Promise<ScanResult>;
22
+ /** Strip // and /* *\/ comments; preserve newlines for line numbers. */
23
+ export declare function stripComments(source: string): string;
22
24
  export declare function extractFromSource(source: string, file: string): ScanResult;
@@ -62,14 +62,77 @@ function extname(name) {
62
62
  const i = name.lastIndexOf('.');
63
63
  return i === -1 ? '' : name.slice(i);
64
64
  }
65
+ /** Strip // and /* *\/ comments; preserve newlines for line numbers. */
66
+ export function stripComments(source) {
67
+ let out = '';
68
+ let i = 0;
69
+ while (i < source.length) {
70
+ const ch = source[i];
71
+ const next = source[i + 1];
72
+ // string / template — copy verbatim so // inside strings stay
73
+ if (ch === '"' || ch === "'" || ch === '`') {
74
+ const quote = ch;
75
+ out += ch;
76
+ i++;
77
+ while (i < source.length) {
78
+ const c = source[i];
79
+ out += c;
80
+ if (c === '\\' && i + 1 < source.length) {
81
+ out += source[i + 1];
82
+ i += 2;
83
+ continue;
84
+ }
85
+ if (c === quote) {
86
+ i++;
87
+ break;
88
+ }
89
+ // template ${ } — shallow skip (good enough for comment strip)
90
+ if (quote === '`' && c === '$' && source[i + 1] === '{') {
91
+ out += '{';
92
+ i += 2;
93
+ let depth = 1;
94
+ while (i < source.length && depth > 0) {
95
+ if (source[i] === '{')
96
+ depth++;
97
+ else if (source[i] === '}')
98
+ depth--;
99
+ out += source[i];
100
+ i++;
101
+ }
102
+ continue;
103
+ }
104
+ i++;
105
+ }
106
+ continue;
107
+ }
108
+ if (ch === '/' && next === '/') {
109
+ while (i < source.length && source[i] !== '\n')
110
+ i++;
111
+ continue;
112
+ }
113
+ if (ch === '/' && next === '*') {
114
+ i += 2;
115
+ while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) {
116
+ out += source[i] === '\n' ? '\n' : ' ';
117
+ i++;
118
+ }
119
+ i += 2;
120
+ continue;
121
+ }
122
+ out += ch;
123
+ i++;
124
+ }
125
+ return out;
126
+ }
65
127
  export function extractFromSource(source, file) {
128
+ const code = stripComments(source);
66
129
  const out = [];
67
130
  const dynamicNamespaces = new Set();
68
131
  const translators = new Map(); // var → ns
69
132
  // useT('ns') | getT('ns') | getT(locale, 'ns')
70
133
  const declRe = /(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?(?:useT|getT)\s*\(\s*(?:[^'"\n,]+,\s*)?(['"])([^'"]*)\2\s*\)/g;
71
134
  let m;
72
- while ((m = declRe.exec(source)) !== null) {
135
+ while ((m = declRe.exec(code)) !== null) {
73
136
  translators.set(m[1], m[3] || '_root');
74
137
  }
75
138
  const push = (ns, key, index) => {
@@ -79,27 +142,26 @@ export function extractFromSource(source, file) {
79
142
  namespace: ns || '_root',
80
143
  key,
81
144
  file,
82
- line: lineAt(source, index),
145
+ line: lineAt(code, index),
83
146
  });
84
147
  };
85
148
  for (const [varName, ns] of translators) {
86
149
  // Literal t('key') / t.rich('key')
87
150
  const callRe = new RegExp(`\\b${escapeRegExp(varName)}(?:\\.rich)?\\s*\\(\\s*(['"])([^'"]+)\\1`, 'g');
88
151
  let cm;
89
- while ((cm = callRe.exec(source)) !== null) {
152
+ while ((cm = callRe.exec(code)) !== null) {
90
153
  push(ns, cm[2], cm.index);
91
154
  }
92
- // t.dynamic(any, { from: ['a', 'b'] })
155
+ // t.dynamic(any, { from: ['a', 'b'] }) — only `from` keys become refs
93
156
  const dynRe = new RegExp(`\\b${escapeRegExp(varName)}\\.dynamic\\s*\\(`, 'g');
94
157
  let dm;
95
- while ((dm = dynRe.exec(source)) !== null) {
96
- const tail = source.slice(dm.index, dm.index + 400);
158
+ while ((dm = dynRe.exec(code)) !== null) {
159
+ const tail = code.slice(dm.index, dm.index + 400);
97
160
  const fromMatch = /from\s*:\s*\[([^\]]*)\]/.exec(tail);
98
161
  if (fromMatch) {
99
162
  const keys = fromMatch[1].match(/['"]([^'"]+)['"]/g) ?? [];
100
163
  for (const raw of keys) {
101
- const key = raw.slice(1, -1);
102
- push(ns, key, dm.index);
164
+ push(ns, raw.slice(1, -1), dm.index);
103
165
  }
104
166
  }
105
167
  dynamicNamespaces.add(ns || '_root');
@@ -107,28 +169,15 @@ export function extractFromSource(source, file) {
107
169
  // t.ns('other') — whole namespace marked dynamic
108
170
  const nsRe = new RegExp(`\\b${escapeRegExp(varName)}\\.ns\\s*\\(\\s*(['"])([^'"]+)\\1`, 'g');
109
171
  let nm;
110
- while ((nm = nsRe.exec(source)) !== null) {
172
+ while ((nm = nsRe.exec(code)) !== null) {
111
173
  dynamicNamespaces.add(nm[2] || '_root');
112
174
  }
113
- // Non-literal t(var) / t.rich(var) — mark namespace dynamic + harvest nearby string arrays
175
+ // Non-literal t(var) — suppress unused for the namespace only.
176
+ // Do NOT harvest unrelated string arrays (cache tags, sort keys, …).
114
177
  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;
178
+ if (nonLitRe.test(code)) {
119
179
  dynamicNamespaces.add(ns || '_root');
120
180
  }
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
- }
131
- }
132
181
  }
133
182
  return { refs: out, dynamicNamespaces };
134
183
  }
@@ -32,9 +32,11 @@ export function withTongue(nextConfig = {}, options = {}) {
32
32
  }
33
33
  // Next 16: content must be RegExp; omit `as` when returning the same module type
34
34
  // (using `as: '*.tsx'` previously doubled extensions → `file.tsx.tsx`).
35
+ // Do NOT gate on `browser` — client components still SSR, and useT must be
36
+ // rewritten on the server graph too (otherwise SSR throws without loadNamespace).
35
37
  const tongueLoaderRule = {
36
38
  condition: {
37
- all: [{ not: 'foreign' }, 'browser', { content: /useT\s*\(/ }],
39
+ all: [{ not: 'foreign' }, { content: /useT\s*\(/ }],
38
40
  },
39
41
  loaders: [
40
42
  {
@@ -65,18 +67,17 @@ export function withTongue(nextConfig = {}, options = {}) {
65
67
  ...(config.resolve.alias ?? {}),
66
68
  '#tongue': outDir,
67
69
  };
68
- if (!ctx.isServer) {
69
- config.module ??= {};
70
- config.module.rules = [
71
- ...(config.module.rules ?? []),
72
- {
73
- test: /\.[jt]sx?$/,
74
- exclude: /node_modules/,
75
- enforce: 'pre',
76
- use: [{ loader: loaderPath, options: { outDir: '.tongue' } }],
77
- },
78
- ];
79
- }
70
+ // Client components SSR under webpack too — apply on both graphs.
71
+ config.module ??= {};
72
+ config.module.rules = [
73
+ ...(config.module.rules ?? []),
74
+ {
75
+ test: /\.[jt]sx?$/,
76
+ exclude: /node_modules/,
77
+ enforce: 'pre',
78
+ use: [{ loader: loaderPath, options: { outDir: '.tongue' } }],
79
+ },
80
+ ];
80
81
  return userWebpack ? userWebpack(config, ctx) : config;
81
82
  };
82
83
  const enableRootParams = peekRootParamsFlag(cwd, options.config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-tongue",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "Compiler-first, type-safe i18n for the Next.js 16 App Router",
5
5
  "license": "MIT",
6
6
  "type": "module",