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
|
-
|
|
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
|
-
|
|
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() {
|
package/dist/client/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 {
|
|
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
|
|
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,
|