next-tongue 0.9.5 → 0.9.6
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.
- package/dist/client/index.js +9 -9
- package/dist/next/loader.d.ts +10 -0
- package/dist/next/loader.js +73 -8
- package/package.json +1 -1
package/dist/client/index.js
CHANGED
|
@@ -82,19 +82,19 @@ export function useT(namespace) {
|
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
|
-
* Stub used when the call-site loader owns message delivery.
|
|
86
|
-
*
|
|
87
|
-
*
|
|
85
|
+
* Stub used when the call-site loader owns message delivery. Literal `t('key')`
|
|
86
|
+
* and `t.dynamic(key, { from })` should be rewritten by the loader; if you still
|
|
87
|
+
* hit this, the rewrite did not run (or a key was not in `from`).
|
|
88
88
|
*/
|
|
89
89
|
function createLoaderStubT(locale, ns) {
|
|
90
|
-
const fail = () => {
|
|
91
|
-
throw new TongueError('TONGUE_E_NO_PROVIDER', `useT('${ns}')
|
|
90
|
+
const fail = (kind) => {
|
|
91
|
+
throw new TongueError('TONGUE_E_NO_PROVIDER', `useT('${ns}') ${kind} was not rewritten to a per-key import. Ensure withTongue() is applied (loader on SSR + client). Prefer getT on the server for static copy; for dynamic keys use t.dynamic(key, { from: [...] }) so the loader can import those keys.`);
|
|
92
92
|
};
|
|
93
|
-
const t = Object.assign((..._args) => fail(), {
|
|
94
|
-
rich: fail,
|
|
93
|
+
const t = Object.assign((..._args) => fail('t()'), {
|
|
94
|
+
rich: () => fail('t.rich()'),
|
|
95
95
|
has: () => false,
|
|
96
|
-
dynamic: fail,
|
|
97
|
-
ns: fail,
|
|
96
|
+
dynamic: () => fail('t.dynamic()'),
|
|
97
|
+
ns: () => fail('t.ns()'),
|
|
98
98
|
});
|
|
99
99
|
void locale;
|
|
100
100
|
return t;
|
package/dist/next/loader.d.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* const t = useT('home'); t('inbox', { count })
|
|
7
7
|
* into a static import of the per-key module when the key is a string literal.
|
|
8
8
|
*
|
|
9
|
+
* Also rewrites `t.dynamic(key, { from: [...] })` to a map of those imports.
|
|
10
|
+
*
|
|
9
11
|
* Escape analysis: if `t` is passed around, falls back to namespace import + warning.
|
|
10
12
|
*/
|
|
11
13
|
export type LoaderOptions = {
|
|
@@ -17,6 +19,14 @@ export type LoaderResult = {
|
|
|
17
19
|
warnings: string[];
|
|
18
20
|
};
|
|
19
21
|
export declare function transformCallSites(source: string, resourcePath: string, _options?: LoaderOptions): LoaderResult;
|
|
22
|
+
/**
|
|
23
|
+
* Peel leading `"use client"` / `"use server"` (and preceding comments/whitespace)
|
|
24
|
+
* so we can re-emit them before any injected imports.
|
|
25
|
+
*/
|
|
26
|
+
export declare function splitLeadingDirectives(source: string): {
|
|
27
|
+
directives: string;
|
|
28
|
+
body: string;
|
|
29
|
+
};
|
|
20
30
|
/** Webpack/Turbopack loader entry. */
|
|
21
31
|
export default function tongueLoader(this: {
|
|
22
32
|
resourcePath: string;
|
package/dist/next/loader.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* const t = useT('home'); t('inbox', { count })
|
|
7
7
|
* into a static import of the per-key module when the key is a string literal.
|
|
8
8
|
*
|
|
9
|
+
* Also rewrites `t.dynamic(key, { from: [...] })` to a map of those imports.
|
|
10
|
+
*
|
|
9
11
|
* Escape analysis: if `t` is passed around, falls back to namespace import + warning.
|
|
10
12
|
*/
|
|
11
13
|
const USE_T_DECL = /(?:const|let|var)\s+(\w+)\s*=\s*useT\s*\(\s*(['"])([^'"]*)\2\s*\)/g;
|
|
@@ -14,10 +16,11 @@ export function transformCallSites(source, resourcePath, _options = {}) {
|
|
|
14
16
|
if (!source.includes('useT(')) {
|
|
15
17
|
return { code: source, warnings };
|
|
16
18
|
}
|
|
19
|
+
const { directives, body: withoutDirs } = splitLeadingDirectives(source);
|
|
17
20
|
const translators = new Map(); // varName → namespace
|
|
18
21
|
let m;
|
|
19
22
|
const declRe = new RegExp(USE_T_DECL.source, 'g');
|
|
20
|
-
while ((m = declRe.exec(
|
|
23
|
+
while ((m = declRe.exec(withoutDirs)) !== null) {
|
|
21
24
|
translators.set(m[1], m[3] || '_root');
|
|
22
25
|
}
|
|
23
26
|
if (translators.size === 0) {
|
|
@@ -25,21 +28,36 @@ export function transformCallSites(source, resourcePath, _options = {}) {
|
|
|
25
28
|
}
|
|
26
29
|
// Escape analysis: if t is used beyond property calls, warn and skip rewrite
|
|
27
30
|
for (const [varName, ns] of translators) {
|
|
28
|
-
if (escapes(
|
|
31
|
+
if (escapes(withoutDirs, varName)) {
|
|
29
32
|
warnings.push(`TONGUE_W_ESCAPE: ${resourcePath}: '${varName}' from useT('${ns}') escapes this file; importing whole namespace`);
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
const imports = new Map();
|
|
33
|
-
let code =
|
|
36
|
+
let code = withoutDirs;
|
|
34
37
|
for (const [varName, ns] of translators) {
|
|
35
|
-
if (escapes(
|
|
38
|
+
if (escapes(withoutDirs, varName))
|
|
36
39
|
continue;
|
|
40
|
+
// Literal t('key') / t.rich('key')
|
|
37
41
|
const callRe = new RegExp(`\\b${escapeRegExp(varName)}(?:\\.rich)?\\s*\\(\\s*(['"])([^'"]+)\\1`, 'g');
|
|
38
42
|
code = code.replace(callRe, (full, _q, key) => {
|
|
39
43
|
const ident = safeImportIdent(ns, key);
|
|
40
44
|
imports.set(`${ns}:${key}`, { ns, key, ident });
|
|
41
45
|
return full;
|
|
42
46
|
});
|
|
47
|
+
// t.dynamic(expr, { from: ['a', 'b'] }, values?)
|
|
48
|
+
const dynRe = new RegExp(`\\b${escapeRegExp(varName)}\\.dynamic\\s*\\(`, 'g');
|
|
49
|
+
let dm;
|
|
50
|
+
while ((dm = dynRe.exec(withoutDirs)) !== null) {
|
|
51
|
+
const tail = withoutDirs.slice(dm.index, dm.index + 500);
|
|
52
|
+
const fromMatch = /from\s*:\s*\[([^\]]*)\]/.exec(tail);
|
|
53
|
+
if (!fromMatch)
|
|
54
|
+
continue;
|
|
55
|
+
const keys = [...fromMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map((x) => x[1]);
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
const ident = safeImportIdent(ns, key);
|
|
58
|
+
imports.set(`${ns}:${key}`, { ns, key, ident });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
43
61
|
}
|
|
44
62
|
if (imports.size === 0) {
|
|
45
63
|
return { code: source, warnings };
|
|
@@ -54,9 +72,29 @@ export function transformCallSites(source, resourcePath, _options = {}) {
|
|
|
54
72
|
const localeVar = '__tongue_locale';
|
|
55
73
|
// Inject locale once after the first useT declaration
|
|
56
74
|
code = code.replace(/((?:const|let|var)\s+\w+\s*=\s*useT\s*\([^)]*\))/, `$1\n const ${localeVar} = useLocale();`);
|
|
57
|
-
//
|
|
75
|
+
// Replace t.dynamic(expr, { from: [...] }, values?)
|
|
76
|
+
for (const [varName, ns] of translators) {
|
|
77
|
+
if (escapes(withoutDirs, varName))
|
|
78
|
+
continue;
|
|
79
|
+
const dynCallRe = new RegExp(`\\b${escapeRegExp(varName)}\\.dynamic\\s*\\(\\s*([^,]+?)\\s*,\\s*\\{[^}]*?from\\s*:\\s*\\[([^\\]]*)][^}]*\\}\\s*(?:,\\s*([^)]*?))?\\)`, 'g');
|
|
80
|
+
code = code.replace(dynCallRe, (_full, keyExpr, fromBody, values) => {
|
|
81
|
+
const keys = [...fromBody.matchAll(/['"]([^'"]+)['"]/g)].map((x) => x[1]);
|
|
82
|
+
const mapEntries = keys
|
|
83
|
+
.map((key) => {
|
|
84
|
+
const ident = safeImportIdent(ns, key);
|
|
85
|
+
imports.set(`${ns}:${key}`, { ns, key, ident });
|
|
86
|
+
return `${JSON.stringify(key)}: ${ident}`;
|
|
87
|
+
})
|
|
88
|
+
.join(', ');
|
|
89
|
+
const valuesArg = values != null && String(values).trim() !== ''
|
|
90
|
+
? `, ${String(values).trim()}`
|
|
91
|
+
: '';
|
|
92
|
+
return `((__tongue_dyn) => { const __tongue_fn = __tongue_dyn[${keyExpr.trim()}]; if (!__tongue_fn) throw new Error(${JSON.stringify(`[next-tongue] t.dynamic key not in from: [${keys.join(', ')}]`)}); return __tongue_fn(${localeVar}${valuesArg}); })({ ${mapEntries} })`;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// Replace t('key', values) / t.rich('key', values) with per-key calls
|
|
58
96
|
for (const [varName, ns] of translators) {
|
|
59
|
-
if (escapes(
|
|
97
|
+
if (escapes(withoutDirs, varName))
|
|
60
98
|
continue;
|
|
61
99
|
for (const { key, ident } of [...imports.values()].filter((i) => i.ns === ns)) {
|
|
62
100
|
const richRe = new RegExp(`\\b${escapeRegExp(varName)}\\.rich\\s*\\(\\s*(['"])${escapeRegExp(key)}\\1\\s*(,\\s*([^)]*))?\\)`, 'g');
|
|
@@ -85,13 +123,40 @@ export function transformCallSites(source, resourcePath, _options = {}) {
|
|
|
85
123
|
preamble += `import { useLocale } from 'next-tongue/client';\n`;
|
|
86
124
|
}
|
|
87
125
|
for (const [varName, ns] of translators) {
|
|
88
|
-
if (!escapes(
|
|
126
|
+
if (!escapes(withoutDirs, varName))
|
|
89
127
|
continue;
|
|
90
128
|
preamble +=
|
|
91
129
|
`process.env.NODE_ENV !== 'production' && globalThis.__tongueDevEscape?.(` +
|
|
92
130
|
`${JSON.stringify(ns || '_root')}, ${JSON.stringify(resourcePath)});\n`;
|
|
93
131
|
}
|
|
94
|
-
|
|
132
|
+
// "use client" / "use server" MUST be the first statement(s) — never after imports.
|
|
133
|
+
return { code: directives + preamble + code, warnings };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Peel leading `"use client"` / `"use server"` (and preceding comments/whitespace)
|
|
137
|
+
* so we can re-emit them before any injected imports.
|
|
138
|
+
*/
|
|
139
|
+
export function splitLeadingDirectives(source) {
|
|
140
|
+
let rest = source.replace(/^\uFEFF/, '');
|
|
141
|
+
const found = [];
|
|
142
|
+
while (rest.length > 0) {
|
|
143
|
+
// leading whitespace / comments
|
|
144
|
+
const lead = rest.match(/^((?:\s|\/\/[^\n]*\n|\/\*[\s\S]*?\*\/)*)/);
|
|
145
|
+
const leadLen = lead?.[0]?.length ?? 0;
|
|
146
|
+
const afterLead = rest.slice(leadLen);
|
|
147
|
+
const dir = afterLead.match(/^['"]use (client|server)['"];?[ \t]*(?:\r?\n)?/);
|
|
148
|
+
if (!dir)
|
|
149
|
+
break;
|
|
150
|
+
found.push(`"use ${dir[1]}";`);
|
|
151
|
+
rest = afterLead.slice(dir[0].length);
|
|
152
|
+
}
|
|
153
|
+
if (found.length === 0) {
|
|
154
|
+
return { directives: '', body: source.replace(/^\uFEFF/, '') };
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
directives: found.join('\n') + '\n\n',
|
|
158
|
+
body: rest,
|
|
159
|
+
};
|
|
95
160
|
}
|
|
96
161
|
function escapes(source, varName) {
|
|
97
162
|
const v = escapeRegExp(varName);
|