next-tongue 0.9.10 → 0.9.12
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/cli/check-types.d.ts +1 -1
- package/dist/cli/check.js +4 -0
- package/dist/cli/init.js +6 -1
- package/dist/compiler/build.js +6 -4
- package/dist/compiler/validate/client-loader.d.ts +7 -0
- package/dist/compiler/validate/client-loader.js +79 -0
- package/dist/next/loader.d.ts +8 -0
- package/dist/next/loader.js +68 -6
- package/dist/next/with-tongue.js +10 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type CheckKind = 'missing' | 'unused' | 'undefined' | 'dynamic' | 'mismatch' | 'syntax' | 'stale' | 'a11y' | 'bidi' | 'length' | 'duplicate' | 'icu';
|
|
1
|
+
export type CheckKind = 'missing' | 'unused' | 'undefined' | 'dynamic' | 'mismatch' | 'syntax' | 'stale' | 'a11y' | 'bidi' | 'length' | 'duplicate' | 'icu' | 'client-loader';
|
|
2
2
|
export type CheckSeverity = 'error' | 'warn';
|
|
3
3
|
export type CheckIssue = {
|
|
4
4
|
check: CheckKind;
|
package/dist/cli/check.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { loadConfig } from '../config/load-config.js';
|
|
2
2
|
import { validateCatalog, validateStale } from '../compiler/validate/catalog.js';
|
|
3
3
|
import { validateUsage } from '../compiler/validate/usage.js';
|
|
4
|
+
import { validateClientLoader } from '../compiler/validate/client-loader.js';
|
|
4
5
|
import { validateA11y } from '../compiler/validate/a11y.js';
|
|
5
6
|
import { validateBidi } from '../compiler/validate/bidi.js';
|
|
7
|
+
import { DEFAULT_SCAN_ROOTS } from '../compiler/scan-keys.js';
|
|
6
8
|
import { loadLocaleMessages } from '../compiler/load-messages.js';
|
|
7
9
|
import { duplicateSourceKeys, loadAndMergeSources, } from '../compiler/sources.js';
|
|
8
10
|
import { validateLength } from '../compiler/validate/length.js';
|
|
@@ -20,6 +22,8 @@ export async function cmdCheck(opts) {
|
|
|
20
22
|
...(localeFilter ? { locales: localeFilter } : {}),
|
|
21
23
|
})));
|
|
22
24
|
issues.push(...(await validateUsage(config, cwd)));
|
|
25
|
+
const scanRoots = config.scan.include.length > 0 ? config.scan.include : DEFAULT_SCAN_ROOTS;
|
|
26
|
+
issues.push(...(await validateClientLoader(cwd, scanRoots)));
|
|
23
27
|
issues.push(...(await validateStale(config, cwd, {
|
|
24
28
|
...(localeFilter ? { locales: localeFilter } : {}),
|
|
25
29
|
})));
|
package/dist/cli/init.js
CHANGED
|
@@ -32,6 +32,9 @@ export async function cmdInit(opts) {
|
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
const appRoot = await detectAppRoot(cwd);
|
|
35
|
+
if (!opts.quiet) {
|
|
36
|
+
console.log(`tongue init: using ${appRoot}/ for App Router files`);
|
|
37
|
+
}
|
|
35
38
|
const hasRootLayout = await hasFile(cwd, `${appRoot}/layout.tsx`, `${appRoot}/layout.ts`, `${appRoot}/layout.jsx`, `${appRoot}/layout.js`);
|
|
36
39
|
const proxyPath = await detectProxyPath(cwd, appRoot);
|
|
37
40
|
const plans = [];
|
|
@@ -69,7 +72,9 @@ export default defineConfig({
|
|
|
69
72
|
});
|
|
70
73
|
}
|
|
71
74
|
else if (!opts.quiet) {
|
|
72
|
-
console.log('tongue init: existing proxy/middleware detected —
|
|
75
|
+
console.log('tongue init: existing proxy/middleware detected — do not overwrite. Compose like:\n\n' +
|
|
76
|
+
proxyScaffold(locales, defaultLocale, prefix) +
|
|
77
|
+
'\nSee docs/routing-and-proxy.md#composing-with-your-own-proxy-logic / hybrid routing.');
|
|
73
78
|
}
|
|
74
79
|
const layoutPath = `${appRoot}/[locale]/layout.tsx`;
|
|
75
80
|
plans.push({
|
package/dist/compiler/build.js
CHANGED
|
@@ -113,8 +113,8 @@ export async function buildMessages(options) {
|
|
|
113
113
|
const defaultMsgRel = config.messages.replaceAll('{locale}', config.defaultLocale);
|
|
114
114
|
const emitLocales = localesForEmit(config);
|
|
115
115
|
// Content hash for idempotency (bump CODEGEN_VERSION when emit format changes)
|
|
116
|
-
// Bump when emit shape changes (reserved idents, auto ensureTongue
|
|
117
|
-
const CODEGEN_VERSION =
|
|
116
|
+
// Bump when emit shape changes (reserved idents, auto ensureTongue, routing export).
|
|
117
|
+
const CODEGEN_VERSION = 10;
|
|
118
118
|
const hash = createHash('sha256')
|
|
119
119
|
.update(JSON.stringify({
|
|
120
120
|
codegen: CODEGEN_VERSION,
|
|
@@ -333,9 +333,9 @@ export async function buildMessages(options) {
|
|
|
333
333
|
!productionLocales.includes(config.pseudo.locale);
|
|
334
334
|
runtimeLines.push(`export const allLocales = ${JSON.stringify([...emitLocales])};`, `const productionLocales = ${JSON.stringify(productionLocales)};`, pseudoDevOnly
|
|
335
335
|
? `export const locales = process.env.NODE_ENV === 'production' ? productionLocales : allLocales;`
|
|
336
|
-
: `export const locales = allLocales;`, `export const defaultLocale = ${JSON.stringify(config.defaultLocale)};`, `export const timeZone = ${JSON.stringify(config.timeZone)};`, config.pseudo && config.pseudo.enabled !== false
|
|
336
|
+
: `export const locales = allLocales;`, `export const defaultLocale = ${JSON.stringify(config.defaultLocale)};`, `export const prefix = ${JSON.stringify(config.routing.prefix)};`, `export const timeZone = ${JSON.stringify(config.timeZone)};`, config.pseudo && config.pseudo.enabled !== false
|
|
337
337
|
? `export const pseudoLocale = ${JSON.stringify(config.pseudo.locale)};`
|
|
338
|
-
: `export const pseudoLocale = undefined;`, `export function localeParams() {`, ` return locales.map((locale) => ({ locale }));`, `}`, `let ready = false;`, `export function ensureTongue() {`, ` if (ready) return;`, ` __setServerRuntime({`, ` locales: allLocales,`, ` defaultLocale,`, ` timeZone,`, ` rootParams: ${config.routing.rootParams ? 'true' : 'false'},`, ` loadNamespace: async (namespace) => {`, ` const messages = namespaces[namespace];`, ` if (!messages) throw new Error('[next-tongue] Unknown namespace: ' + namespace);`, ` return messages;`, ` },`, ` });`, config.pseudo && config.pseudo.enabled !== false
|
|
338
|
+
: `export const pseudoLocale = undefined;`, `/** Single source of truth for createProxy / createNavigation / TongueShell. */`, `export const routing = { locales, defaultLocale, prefix };`, `export function localeParams() {`, ` return locales.map((locale) => ({ locale }));`, `}`, `let ready = false;`, `export function ensureTongue() {`, ` if (ready) return;`, ` __setServerRuntime({`, ` locales: allLocales,`, ` defaultLocale,`, ` timeZone,`, ` rootParams: ${config.routing.rootParams ? 'true' : 'false'},`, ` loadNamespace: async (namespace) => {`, ` const messages = namespaces[namespace];`, ` if (!messages) throw new Error('[next-tongue] Unknown namespace: ' + namespace);`, ` return messages;`, ` },`, ` });`, config.pseudo && config.pseudo.enabled !== false
|
|
339
339
|
? ` configureFormatMessage({ timeZone, pseudo: { locale: ${JSON.stringify(config.pseudo.locale)}, expand: ${config.pseudo.expand} } });`
|
|
340
340
|
: ` configureFormatMessage({ timeZone });`, ` ready = true;`, `}`,
|
|
341
341
|
// Side-effect: importing `#tongue/runtime` (or getT's lazy import) wires the
|
|
@@ -347,8 +347,10 @@ export async function buildMessages(options) {
|
|
|
347
347
|
`export declare const allLocales: readonly string[];`,
|
|
348
348
|
`export declare const locales: readonly string[];`,
|
|
349
349
|
`export declare const defaultLocale: string;`,
|
|
350
|
+
`export declare const prefix: 'always' | 'as-needed' | 'never';`,
|
|
350
351
|
`export declare const timeZone: string;`,
|
|
351
352
|
`export declare const pseudoLocale: string | undefined;`,
|
|
353
|
+
`export declare const routing: { locales: readonly string[]; defaultLocale: string; prefix: 'always' | 'as-needed' | 'never' };`,
|
|
352
354
|
`export declare function localeParams(): { locale: string }[];`,
|
|
353
355
|
`export declare function ensureTongue(): void;`,
|
|
354
356
|
``,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CheckIssue } from '../../cli/check-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Dry-run the call-site loader on `"use client"` files that call `useT`.
|
|
4
|
+
* Fails when rewritten output calls `useLocale` without importing it
|
|
5
|
+
* (Safari: Can't find variable: useLocale).
|
|
6
|
+
*/
|
|
7
|
+
export declare function validateClientLoader(cwd: string, roots?: readonly string[]): Promise<CheckIssue[]>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { transformCallSites } from '../../next/loader.js';
|
|
4
|
+
import { DEFAULT_SCAN_ROOTS } from '../scan-keys.js';
|
|
5
|
+
const SOURCE_EXT = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
6
|
+
/**
|
|
7
|
+
* Dry-run the call-site loader on `"use client"` files that call `useT`.
|
|
8
|
+
* Fails when rewritten output calls `useLocale` without importing it
|
|
9
|
+
* (Safari: Can't find variable: useLocale).
|
|
10
|
+
*/
|
|
11
|
+
export async function validateClientLoader(cwd, roots = DEFAULT_SCAN_ROOTS) {
|
|
12
|
+
const issues = [];
|
|
13
|
+
const files = await collectSourceFiles(cwd, roots);
|
|
14
|
+
for (const abs of files) {
|
|
15
|
+
let src;
|
|
16
|
+
try {
|
|
17
|
+
src = await readFile(abs, 'utf8');
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (!/"use client"\s*;?/.test(src) && !/'use client'\s*;?/.test(src)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!/\buseT\s*\(/.test(src))
|
|
26
|
+
continue;
|
|
27
|
+
const rel = relative(cwd, abs);
|
|
28
|
+
const { code } = transformCallSites(src, rel);
|
|
29
|
+
if (!/\buseLocale\s*\(/.test(code))
|
|
30
|
+
continue;
|
|
31
|
+
const hasImport = /import\s*\{[^}]*\buseLocale\b[^}]*\}\s*from\s*['"]next-tongue\/client['"]/.test(code);
|
|
32
|
+
if (!hasImport) {
|
|
33
|
+
issues.push({
|
|
34
|
+
check: 'client-loader',
|
|
35
|
+
severity: 'error',
|
|
36
|
+
message: `Rewritten useT in "${rel}" calls useLocale() without importing it from 'next-tongue/client' (Safari ReferenceError). Upgrade next-tongue / ensure withTongue() applies the call-site loader.`,
|
|
37
|
+
file: rel,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return issues;
|
|
42
|
+
}
|
|
43
|
+
async function collectSourceFiles(cwd, roots) {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const root of roots) {
|
|
46
|
+
const abs = join(cwd, root);
|
|
47
|
+
try {
|
|
48
|
+
if (!(await stat(abs)).isDirectory())
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
await walk(abs, out);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
async function walk(dir, out) {
|
|
59
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
60
|
+
for (const ent of entries) {
|
|
61
|
+
if (ent.name.startsWith('.'))
|
|
62
|
+
continue;
|
|
63
|
+
const abs = join(dir, ent.name);
|
|
64
|
+
if (ent.isDirectory()) {
|
|
65
|
+
if (ent.name === 'node_modules' ||
|
|
66
|
+
ent.name === '.next' ||
|
|
67
|
+
ent.name === 'dist') {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
await walk(abs, out);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const i = ent.name.lastIndexOf('.');
|
|
74
|
+
const ext = i === -1 ? '' : ent.name.slice(i);
|
|
75
|
+
if (SOURCE_EXT.has(ext))
|
|
76
|
+
out.push(abs);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
package/dist/next/loader.d.ts
CHANGED
|
@@ -28,6 +28,14 @@ export declare function splitLeadingDirectives(source: string): {
|
|
|
28
28
|
directives: string;
|
|
29
29
|
body: string;
|
|
30
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Ensure `useLocale` is a real import. Looking for the identifier in the body is
|
|
33
|
+
* not enough — the loader injects `useLocale()` calls before this runs.
|
|
34
|
+
*/
|
|
35
|
+
export declare function ensureUseLocaleImport(code: string, preamble: string): {
|
|
36
|
+
code: string;
|
|
37
|
+
preamble: string;
|
|
38
|
+
};
|
|
31
39
|
/** Webpack/Turbopack loader entry. */
|
|
32
40
|
export default function tongueLoader(this: {
|
|
33
41
|
resourcePath: string;
|
package/dist/next/loader.js
CHANGED
|
@@ -67,13 +67,25 @@ export function transformCallSites(source, resourcePath, _options = {}) {
|
|
|
67
67
|
importLines.push(`import { ${safeExport(key)} as ${ident} } from '${path}';`);
|
|
68
68
|
loadedCalls.push(`process.env.NODE_ENV !== 'production' && globalThis.__tongueDevLoaded?.(${JSON.stringify(ns || '_root')}, ${JSON.stringify(key)});`);
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
|
|
70
|
+
// Inject locale after every useT — one binding per translator var so each
|
|
71
|
+
// component (and multi-useT in one function) has __tongue_locale_* in scope.
|
|
72
|
+
for (const varName of translators.keys()) {
|
|
73
|
+
if (escapes(withoutDirs, varName))
|
|
74
|
+
continue;
|
|
75
|
+
const localeVar = localeVarFor(varName);
|
|
76
|
+
const declRe = new RegExp(`((?:const|let|var)\\s+${escapeRegExp(varName)}\\s*=\\s*useT\\s*\\([^)]*\\))`, 'g');
|
|
77
|
+
code = code.replace(declRe, (full) => {
|
|
78
|
+
// Idempotent if a prior pass already injected.
|
|
79
|
+
if (full.includes(localeVar))
|
|
80
|
+
return full;
|
|
81
|
+
return `${full}\n const ${localeVar} = useLocale();`;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
73
84
|
// Replace t.dynamic(expr, { from }, values?) — rewrite from the end so indices stay valid
|
|
74
85
|
for (const [varName, ns] of translators) {
|
|
75
86
|
if (escapes(withoutDirs, varName))
|
|
76
87
|
continue;
|
|
88
|
+
const localeVar = localeVarFor(varName);
|
|
77
89
|
const calls = findDynamicCalls(code, varName).filter((c) => c.keys && c.keys.length > 0);
|
|
78
90
|
for (let ci = calls.length - 1; ci >= 0; ci--) {
|
|
79
91
|
const call = calls[ci];
|
|
@@ -96,6 +108,7 @@ export function transformCallSites(source, resourcePath, _options = {}) {
|
|
|
96
108
|
for (const [varName, ns] of translators) {
|
|
97
109
|
if (escapes(withoutDirs, varName))
|
|
98
110
|
continue;
|
|
111
|
+
const localeVar = localeVarFor(varName);
|
|
99
112
|
for (const { key, ident } of [...imports.values()].filter((i) => i.ns === ns)) {
|
|
100
113
|
const richRe = new RegExp(`\\b${escapeRegExp(varName)}\\.rich\\s*\\(\\s*(['"])${escapeRegExp(key)}\\1\\s*(,\\s*([^)]*))?\\)`, 'g');
|
|
101
114
|
code = code.replace(richRe, (_full, _q, _rest, values) => {
|
|
@@ -119,9 +132,9 @@ export function transformCallSites(source, resourcePath, _options = {}) {
|
|
|
119
132
|
if (loadedCalls.length > 0) {
|
|
120
133
|
preamble += loadedCalls.join('\n') + '\n';
|
|
121
134
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
135
|
+
// Injected `useLocale()` calls do NOT count as an import — merge into the
|
|
136
|
+
// existing next-tongue/client import or add one (Safari ReferenceError otherwise).
|
|
137
|
+
({ code, preamble } = ensureUseLocaleImport(code, preamble));
|
|
125
138
|
for (const [varName, ns] of translators) {
|
|
126
139
|
if (!escapes(withoutDirs, varName))
|
|
127
140
|
continue;
|
|
@@ -178,6 +191,55 @@ function escapes(source, varName) {
|
|
|
178
191
|
function escapeRegExp(s) {
|
|
179
192
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
180
193
|
}
|
|
194
|
+
/** Per-useT locale binding so each component function has its own in-scope var. */
|
|
195
|
+
function localeVarFor(varName) {
|
|
196
|
+
return varName === 't' ? '__tongue_locale' : `__tongue_locale_${varName}`;
|
|
197
|
+
}
|
|
198
|
+
const USE_LOCALE_IMPORT = /import\s*\{([^}]*)\}\s*from\s*['"]next-tongue\/client['"]/;
|
|
199
|
+
/**
|
|
200
|
+
* Ensure `useLocale` is a real import. Looking for the identifier in the body is
|
|
201
|
+
* not enough — the loader injects `useLocale()` calls before this runs.
|
|
202
|
+
*/
|
|
203
|
+
export function ensureUseLocaleImport(code, preamble) {
|
|
204
|
+
const combined = preamble + code;
|
|
205
|
+
if (USE_LOCALE_IMPORT.test(combined) && /\buseLocale\b/.test(combined.match(USE_LOCALE_IMPORT)?.[1] ?? '')) {
|
|
206
|
+
return { code, preamble };
|
|
207
|
+
}
|
|
208
|
+
if (USE_LOCALE_IMPORT.test(code)) {
|
|
209
|
+
return {
|
|
210
|
+
code: code.replace(USE_LOCALE_IMPORT, (full, specs) => {
|
|
211
|
+
if (/\buseLocale\b/.test(specs))
|
|
212
|
+
return full;
|
|
213
|
+
const parts = specs
|
|
214
|
+
.split(',')
|
|
215
|
+
.map((s) => s.trim())
|
|
216
|
+
.filter(Boolean);
|
|
217
|
+
parts.push('useLocale');
|
|
218
|
+
return full.replace(specs, ` ${parts.join(', ')} `);
|
|
219
|
+
}),
|
|
220
|
+
preamble,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (USE_LOCALE_IMPORT.test(preamble)) {
|
|
224
|
+
return {
|
|
225
|
+
code,
|
|
226
|
+
preamble: preamble.replace(USE_LOCALE_IMPORT, (full, specs) => {
|
|
227
|
+
if (/\buseLocale\b/.test(specs))
|
|
228
|
+
return full;
|
|
229
|
+
const parts = specs
|
|
230
|
+
.split(',')
|
|
231
|
+
.map((s) => s.trim())
|
|
232
|
+
.filter(Boolean);
|
|
233
|
+
parts.push('useLocale');
|
|
234
|
+
return full.replace(specs, ` ${parts.join(', ')} `);
|
|
235
|
+
}),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
code,
|
|
240
|
+
preamble: preamble + `import { useLocale } from 'next-tongue/client';\n`,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
181
243
|
function safeImportIdent(ns, key) {
|
|
182
244
|
return `__tongue_${(ns + '_' + key).replace(/[^A-Za-z0-9_]/g, '_')}`;
|
|
183
245
|
}
|
package/dist/next/with-tongue.js
CHANGED
|
@@ -18,6 +18,7 @@ export function withTongue(nextConfig = {}, options = {}) {
|
|
|
18
18
|
const input = nextConfig;
|
|
19
19
|
if (!options.skipBuild) {
|
|
20
20
|
runCodegenSync(cwd, options.config);
|
|
21
|
+
warnMissingTongueRuntime(cwd);
|
|
21
22
|
void loadConfig({
|
|
22
23
|
cwd,
|
|
23
24
|
...(options.config != null ? { configPath: options.config } : {}),
|
|
@@ -33,6 +34,9 @@ export function withTongue(nextConfig = {}, options = {}) {
|
|
|
33
34
|
console.warn('[next-tongue] Could not start watcher:', err instanceof Error ? err.message : err);
|
|
34
35
|
});
|
|
35
36
|
}
|
|
37
|
+
else {
|
|
38
|
+
warnMissingTongueRuntime(cwd);
|
|
39
|
+
}
|
|
36
40
|
// Next 16: content must be RegExp; omit `as` when returning the same module type
|
|
37
41
|
// (using `as: '*.tsx'` previously doubled extensions → `file.tsx.tsx`).
|
|
38
42
|
// Do NOT gate on `browser` — client components still SSR, and useT must be
|
|
@@ -171,6 +175,12 @@ function resolveLoaderPath() {
|
|
|
171
175
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
172
176
|
return resolve(here, 'loader.js');
|
|
173
177
|
}
|
|
178
|
+
function warnMissingTongueRuntime(cwd) {
|
|
179
|
+
const runtime = join(cwd, '.tongue', 'runtime.js');
|
|
180
|
+
if (!existsSync(runtime)) {
|
|
181
|
+
console.warn('[next-tongue] `.tongue/runtime.js` is missing. Run `npx tongue build` (or use `"build": "tongue build && next build"`). `next dev` without a recent build leaves getT/useT unwired.');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
174
184
|
function runCodegenSync(cwd, configPath) {
|
|
175
185
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
176
186
|
const bin = resolve(here, '../../bin/tongue.js');
|