next-tongue 0.9.15 → 0.9.16
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/init.js +5 -3
- package/dist/client/index.js +1 -1
- package/dist/compiler/build.js +1 -1
- package/dist/compiler/dynamic-from.js +18 -1
- package/dist/compiler/scan-keys.js +56 -3
- package/dist/errors.d.ts +3 -1
- package/dist/navigation/config.d.ts +6 -0
- package/dist/navigation/config.js +1 -0
- package/dist/navigation/index.d.ts +2 -6
- package/dist/navigation/locale-switcher.d.ts +1 -1
- package/dist/navigation/server.d.ts +39 -0
- package/dist/navigation/server.js +55 -0
- package/dist/server/index.d.ts +17 -4
- package/dist/server/index.js +51 -32
- package/package.json +6 -1
package/dist/cli/init.js
CHANGED
|
@@ -188,6 +188,7 @@ export default withTongue(nextConfig);
|
|
|
188
188
|
ts.compilerOptions = ts.compilerOptions ?? {};
|
|
189
189
|
const paths = { ...(ts.compilerOptions.paths ?? {}) };
|
|
190
190
|
if (!paths['#tongue/*']) {
|
|
191
|
+
// TypeScript accepts extensionless; Node package imports need *.js (below).
|
|
191
192
|
paths['#tongue/*'] = ['./.tongue/*'];
|
|
192
193
|
}
|
|
193
194
|
ts.compilerOptions.paths = paths;
|
|
@@ -227,7 +228,8 @@ export default withTongue(nextConfig);
|
|
|
227
228
|
const raw = await readFile(pkgPath, 'utf8');
|
|
228
229
|
try {
|
|
229
230
|
const pkg = JSON.parse(raw);
|
|
230
|
-
|
|
231
|
+
// Node ESM requires the .js suffix in the mapping target.
|
|
232
|
+
pkg.imports = { ...(pkg.imports ?? {}), '#tongue/*': './.tongue/*.js' };
|
|
231
233
|
const next = `${JSON.stringify(pkg, null, 2)}\n`;
|
|
232
234
|
if (next !== raw) {
|
|
233
235
|
plans.push({ path: 'package.json', kind: 'merge', content: next });
|
|
@@ -292,7 +294,7 @@ import { TongueProvider } from 'next-tongue/client';
|
|
|
292
294
|
import { getDir, isLocale } from 'next-tongue/server';
|
|
293
295
|
import { localeParams } from '#tongue/runtime';
|
|
294
296
|
|
|
295
|
-
//
|
|
297
|
+
// Required: static #tongue/runtime import wires ensureTongue for getT.
|
|
296
298
|
export const generateStaticParams = localeParams;
|
|
297
299
|
|
|
298
300
|
export default async function LocaleLayout({
|
|
@@ -324,7 +326,7 @@ import { TongueProvider } from 'next-tongue/client';
|
|
|
324
326
|
import { isLocale } from 'next-tongue/server';
|
|
325
327
|
import { localeParams } from '#tongue/runtime';
|
|
326
328
|
|
|
327
|
-
//
|
|
329
|
+
// Required: static #tongue/runtime import wires ensureTongue for getT.
|
|
328
330
|
export const generateStaticParams = localeParams;
|
|
329
331
|
|
|
330
332
|
export default async function LocaleLayout({
|
package/dist/client/index.js
CHANGED
|
@@ -88,7 +88,7 @@ export function useT(namespace) {
|
|
|
88
88
|
*/
|
|
89
89
|
function createLoaderStubT(locale, ns) {
|
|
90
90
|
const fail = (kind) => {
|
|
91
|
-
throw new TongueError('
|
|
91
|
+
throw new TongueError('TONGUE_E_NOT_REWRITTEN', `[next-tongue] useT('${ns}') ${kind} was not rewritten to a per-key import (this is not a missing TongueProvider). Ensure withTongue() applies the loader on SSR + client. For t.dynamic use { from: ['a','b'] }, { from: KEYS }, or { from: [...KEYS] } with a same-file const array — imported from-arrays are not resolved.`);
|
|
92
92
|
};
|
|
93
93
|
const t = Object.assign((..._args) => fail('t()'), {
|
|
94
94
|
rich: () => fail('t.rich()'),
|
package/dist/compiler/build.js
CHANGED
|
@@ -412,7 +412,7 @@ export async function buildMessages(options) {
|
|
|
412
412
|
await writeFile(join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
|
|
413
413
|
await writeFile(join(outDir, '.hash'), hash, 'utf8');
|
|
414
414
|
// package.json imports helper note — apps map "#tongue/*" themselves / via withTongue
|
|
415
|
-
await writeFile(join(outDir, 'README.md'), 'Generated by next-tongue. Do not edit.
|
|
415
|
+
await writeFile(join(outDir, 'README.md'), 'Generated by next-tongue. Do not edit.\nMap package.json `"#tongue/*": "./.tongue/*.js"` (Node needs the .js suffix) and tsconfig paths `"#tongue/*": ["./.tongue/*"]`.\nStatically import `#tongue/runtime` in app/[locale]/layout (e.g. localeParams) so ensureTongue() shares the server module graph with getT.\n', 'utf8');
|
|
416
416
|
await clearOutputDir(publishedOutDir);
|
|
417
417
|
try {
|
|
418
418
|
await rename(outDir, publishedOutDir);
|
|
@@ -91,7 +91,24 @@ export function resolveFromKeys(fileSource, fromExpr) {
|
|
|
91
91
|
const arr = takeBalanced(trimmed, 0, '[', ']');
|
|
92
92
|
if (!arr)
|
|
93
93
|
return null;
|
|
94
|
-
|
|
94
|
+
const inner = arr.slice(1, -1);
|
|
95
|
+
const literals = extractStringLiterals(inner);
|
|
96
|
+
// `{ from: [...NAV_KEYS] }` or `{ from: ['a', ...REST] }`
|
|
97
|
+
const spreadIds = [
|
|
98
|
+
...inner.matchAll(/\.\.\.\s*([A-Za-z_$][\w$]*)/g),
|
|
99
|
+
].map((m) => m[1]);
|
|
100
|
+
if (spreadIds.length > 0) {
|
|
101
|
+
const fromSpreads = [];
|
|
102
|
+
for (const id of spreadIds) {
|
|
103
|
+
const keys = resolveConstArrayKeys(fileSource, id);
|
|
104
|
+
if (!keys?.length)
|
|
105
|
+
return null;
|
|
106
|
+
fromSpreads.push(...keys);
|
|
107
|
+
}
|
|
108
|
+
const merged = [...literals, ...fromSpreads];
|
|
109
|
+
return merged.length > 0 ? merged : null;
|
|
110
|
+
}
|
|
111
|
+
return literals.length > 0 ? literals : null;
|
|
95
112
|
}
|
|
96
113
|
const ov = /^Object\.values\(\s*([A-Za-z_$][\w$]*)\s*\)$/.exec(trimmed);
|
|
97
114
|
if (ov) {
|
|
@@ -142,12 +142,17 @@ export function extractFromSource(source, file) {
|
|
|
142
142
|
const dynamicNamespaces = new Set();
|
|
143
143
|
const undeclaredDynamics = [];
|
|
144
144
|
const translators = new Map();
|
|
145
|
-
// useT('ns') | getT('ns') | getT(locale, 'ns')
|
|
146
|
-
const
|
|
145
|
+
// useT('ns') | getT('ns') | getT(locale, 'ns') — tolerate type annotations,
|
|
146
|
+
// multiline args, and trailing `as const` on the namespace literal.
|
|
147
|
+
const declRe = /(?:const|let|var)\s+(\w+)\s*(?::[^=;]+)?=\s*(?:await\s+)?(useT|getT)\s*\(/g;
|
|
147
148
|
let m;
|
|
148
149
|
while ((m = declRe.exec(code)) !== null) {
|
|
150
|
+
const open = m.index + m[0].length - 1;
|
|
151
|
+
const ns = namespaceFromCallArgs(code, open);
|
|
152
|
+
if (ns == null)
|
|
153
|
+
continue;
|
|
149
154
|
translators.set(m[1], {
|
|
150
|
-
ns:
|
|
155
|
+
ns: ns || '_root',
|
|
151
156
|
kind: m[2],
|
|
152
157
|
});
|
|
153
158
|
}
|
|
@@ -206,6 +211,54 @@ export function extractFromSource(source, file) {
|
|
|
206
211
|
}
|
|
207
212
|
return { refs: out, dynamicNamespaces, undeclaredDynamics };
|
|
208
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Last string-literal argument to getT/useT is the namespace
|
|
216
|
+
* (`getT(locale, 'home')` → home, `useT('nav')` → nav).
|
|
217
|
+
*/
|
|
218
|
+
function namespaceFromCallArgs(source, openParen) {
|
|
219
|
+
let i = openParen + 1;
|
|
220
|
+
let depth = 1;
|
|
221
|
+
let inStr = null;
|
|
222
|
+
let escape = false;
|
|
223
|
+
let lastString = null;
|
|
224
|
+
let buf = '';
|
|
225
|
+
for (; i < source.length; i++) {
|
|
226
|
+
const c = source[i];
|
|
227
|
+
if (inStr) {
|
|
228
|
+
if (escape) {
|
|
229
|
+
escape = false;
|
|
230
|
+
buf += c;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (c === '\\') {
|
|
234
|
+
escape = true;
|
|
235
|
+
buf += c;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (c === inStr) {
|
|
239
|
+
lastString = buf;
|
|
240
|
+
inStr = null;
|
|
241
|
+
buf = '';
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
buf += c;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (c === '"' || c === "'") {
|
|
248
|
+
inStr = c;
|
|
249
|
+
buf = '';
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (c === '(' || c === '[' || c === '{')
|
|
253
|
+
depth++;
|
|
254
|
+
else if (c === ')' || c === ']' || c === '}') {
|
|
255
|
+
depth--;
|
|
256
|
+
if (depth === 0)
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return lastString;
|
|
261
|
+
}
|
|
209
262
|
function lineAt(source, index) {
|
|
210
263
|
let line = 1;
|
|
211
264
|
for (let i = 0; i < index && i < source.length; i++) {
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export type TongueErrorCode = 'TONGUE_E_CONFIG' | 'TONGUE_E_PARSE' | 'TONGUE_E_ICU_SYNTAX' | 'TONGUE_E_UNSUPPORTED_ICU' | 'TONGUE_E_TYPES_MISSING' | 'TONGUE_E_LOCALE' | 'TONGUE_E_IO' | 'TONGUE_E_BUILD' | 'TONGUE_E_NO_PROVIDER'
|
|
1
|
+
export type TongueErrorCode = 'TONGUE_E_CONFIG' | 'TONGUE_E_PARSE' | 'TONGUE_E_ICU_SYNTAX' | 'TONGUE_E_UNSUPPORTED_ICU' | 'TONGUE_E_TYPES_MISSING' | 'TONGUE_E_LOCALE' | 'TONGUE_E_IO' | 'TONGUE_E_BUILD' | 'TONGUE_E_NO_PROVIDER'
|
|
2
|
+
/** Call-site loader did not rewrite `t()` / `t.dynamic()` (not a missing TongueProvider). */
|
|
3
|
+
| 'TONGUE_E_NOT_REWRITTEN';
|
|
2
4
|
export declare class TongueError extends Error {
|
|
3
5
|
readonly code: TongueErrorCode;
|
|
4
6
|
readonly file?: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import NextLink from 'next/link';
|
|
2
2
|
import type { ComponentProps, ReactNode } from 'react';
|
|
3
3
|
import type { Locale, Pathname } from '../index.js';
|
|
4
|
+
import type { NavigationConfig } from './config.js';
|
|
4
5
|
import { getPathname as getPathnameImpl, stripLocalePrefix, toInternalPathname, type PathnameInput } from './pathname.js';
|
|
5
6
|
export type { PathnameInput };
|
|
7
|
+
export type { NavigationConfig } from './config.js';
|
|
6
8
|
export { getPathnameImpl as getPathname, stripLocalePrefix, toInternalPathname };
|
|
7
9
|
export { buildLocaleSwitcherOptions, createLocaleSwitcher, } from './locale-switcher.js';
|
|
8
10
|
export type { LocaleSwitcherOption, UseLocaleSwitcherResult, } from './locale-switcher.js';
|
|
9
|
-
export type NavigationConfig = {
|
|
10
|
-
locales: readonly string[];
|
|
11
|
-
defaultLocale: string;
|
|
12
|
-
prefix?: 'always' | 'as-needed' | 'never';
|
|
13
|
-
pathnames?: Record<string, Partial<Record<string, string>>>;
|
|
14
|
-
};
|
|
15
11
|
type Href = Pathname | PathnameInput;
|
|
16
12
|
/**
|
|
17
13
|
* Create locale-aware navigation helpers bound to routing config.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-safe navigation helpers. `next-tongue/navigation` is a Client Module
|
|
3
|
+
* (`createNavigation` uses hooks) — importing it into a Server Component fails.
|
|
4
|
+
* Use this entry for Footer / layout / not-found Link + redirect.
|
|
5
|
+
*
|
|
6
|
+
* Under `prefix: 'as-needed'`, plain `next/link` with unprefixed hrefs is also fine.
|
|
7
|
+
*/
|
|
8
|
+
import NextLink from 'next/link';
|
|
9
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
10
|
+
import type { Locale, Pathname } from '../index.js';
|
|
11
|
+
import { getPathname as getPathnameImpl, stripLocalePrefix, toInternalPathname, type PathnameInput } from './pathname.js';
|
|
12
|
+
import type { NavigationConfig } from './config.js';
|
|
13
|
+
export type { PathnameInput, NavigationConfig };
|
|
14
|
+
export { getPathnameImpl as getPathname, stripLocalePrefix, toInternalPathname, };
|
|
15
|
+
type Href = Pathname | PathnameInput;
|
|
16
|
+
/**
|
|
17
|
+
* Server-only `createNavigation`: `Link` requires an explicit `locale` prop
|
|
18
|
+
* (no `useLocale`). For client hooks use `next-tongue/navigation`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createNavigation(config: NavigationConfig): {
|
|
21
|
+
Link: (props: Omit<ComponentProps<typeof NextLink>, "href"> & {
|
|
22
|
+
href: Href;
|
|
23
|
+
/** Required on the server — there is no TongueProvider hook here. */
|
|
24
|
+
locale: Locale;
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
}) => import("react").JSX.Element;
|
|
27
|
+
redirect: (args: {
|
|
28
|
+
href: Href;
|
|
29
|
+
locale: Locale;
|
|
30
|
+
}) => never;
|
|
31
|
+
permanentRedirect: (args: {
|
|
32
|
+
href: Href;
|
|
33
|
+
locale: Locale;
|
|
34
|
+
}) => never;
|
|
35
|
+
getPathname: (args: {
|
|
36
|
+
href: Href;
|
|
37
|
+
locale: Locale;
|
|
38
|
+
}) => string;
|
|
39
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Server-safe navigation helpers. `next-tongue/navigation` is a Client Module
|
|
4
|
+
* (`createNavigation` uses hooks) — importing it into a Server Component fails.
|
|
5
|
+
* Use this entry for Footer / layout / not-found Link + redirect.
|
|
6
|
+
*
|
|
7
|
+
* Under `prefix: 'as-needed'`, plain `next/link` with unprefixed hrefs is also fine.
|
|
8
|
+
*/
|
|
9
|
+
import NextLink from 'next/link';
|
|
10
|
+
import { redirect as nextRedirect, permanentRedirect as nextPermanentRedirect, } from 'next/navigation';
|
|
11
|
+
import { getPathname as getPathnameImpl, stripLocalePrefix, toInternalPathname, } from './pathname.js';
|
|
12
|
+
export { getPathnameImpl as getPathname, stripLocalePrefix, toInternalPathname, };
|
|
13
|
+
function bindConfig(config) {
|
|
14
|
+
return {
|
|
15
|
+
locales: config.locales,
|
|
16
|
+
defaultLocale: config.defaultLocale,
|
|
17
|
+
prefix: config.prefix ?? 'as-needed',
|
|
18
|
+
pathnames: config.pathnames,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Server-only `createNavigation`: `Link` requires an explicit `locale` prop
|
|
23
|
+
* (no `useLocale`). For client hooks use `next-tongue/navigation`.
|
|
24
|
+
*/
|
|
25
|
+
export function createNavigation(config) {
|
|
26
|
+
const bound = bindConfig(config);
|
|
27
|
+
function resolve(href, locale) {
|
|
28
|
+
const args = {
|
|
29
|
+
href: href,
|
|
30
|
+
locale,
|
|
31
|
+
locales: bound.locales,
|
|
32
|
+
defaultLocale: bound.defaultLocale,
|
|
33
|
+
prefix: bound.prefix,
|
|
34
|
+
};
|
|
35
|
+
if (bound.pathnames)
|
|
36
|
+
args.pathnames = bound.pathnames;
|
|
37
|
+
return getPathnameImpl(args);
|
|
38
|
+
}
|
|
39
|
+
function Link(props) {
|
|
40
|
+
const { href, locale, ...rest } = props;
|
|
41
|
+
return _jsx(NextLink, { href: resolve(href, locale), ...rest });
|
|
42
|
+
}
|
|
43
|
+
function redirect(args) {
|
|
44
|
+
nextRedirect(resolve(args.href, args.locale));
|
|
45
|
+
}
|
|
46
|
+
function permanentRedirect(args) {
|
|
47
|
+
nextPermanentRedirect(resolve(args.href, args.locale));
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
Link,
|
|
51
|
+
redirect,
|
|
52
|
+
permanentRedirect,
|
|
53
|
+
getPathname: (args) => resolve(args.href, args.locale),
|
|
54
|
+
};
|
|
55
|
+
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -2,7 +2,19 @@ import { type Format } from '../runtime/format.js';
|
|
|
2
2
|
import { type MessageFn, type TFunction } from '../runtime/create-t.js';
|
|
3
3
|
import { getDir, getOgLocale } from '../runtime/locale.js';
|
|
4
4
|
import type { Locale, Namespace } from '../index.js';
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Shared across duplicated `next-tongue/server` module instances (Turbopack/webpack
|
|
7
|
+
* can load the package once via the app graph and again via `#tongue/runtime`'s
|
|
8
|
+
* dynamic import). Module-local `let` would leave getT seeing an unwired copy.
|
|
9
|
+
*/
|
|
10
|
+
type ServerRuntimeState = {
|
|
11
|
+
locales: readonly string[];
|
|
12
|
+
defaultLocale: string;
|
|
13
|
+
timeZone: string;
|
|
14
|
+
rootParams: boolean;
|
|
15
|
+
rootLocaleReader: (() => Promise<string>) | undefined;
|
|
16
|
+
namespaceLoader: ((namespace: string) => Promise<Record<string, MessageFn>>) | undefined;
|
|
17
|
+
};
|
|
6
18
|
/** @internal Used by generated `.tongue/runtime.js` / tests. */
|
|
7
19
|
export declare function __setServerRuntime(opts: {
|
|
8
20
|
locales: readonly string[];
|
|
@@ -11,7 +23,7 @@ export declare function __setServerRuntime(opts: {
|
|
|
11
23
|
rootParams?: boolean;
|
|
12
24
|
/** Injected in tests instead of `next/root-params`. */
|
|
13
25
|
readRootLocale?: () => Promise<string>;
|
|
14
|
-
loadNamespace?:
|
|
26
|
+
loadNamespace?: ServerRuntimeState['namespaceLoader'];
|
|
15
27
|
}): void;
|
|
16
28
|
/** Whether `routing.rootParams` is enabled after `ensureTongue()`. */
|
|
17
29
|
export declare function isRootParamsEnabled(): boolean;
|
|
@@ -39,8 +51,9 @@ export type MultiT<Ns extends readonly Namespace[]> = {
|
|
|
39
51
|
* With `routing.rootParams: true`, `getT(ns)` resolves locale from `next/root-params`
|
|
40
52
|
* when the single argument is a configured namespace (not a locale tag).
|
|
41
53
|
*
|
|
42
|
-
* `
|
|
43
|
-
* `
|
|
54
|
+
* Prefer a static `import { localeParams } from '#tongue/runtime'` in the
|
|
55
|
+
* `[locale]` layout so codegen and getT share one module graph. Lazy load via
|
|
56
|
+
* getT still works (runtime state lives on `globalThis`).
|
|
44
57
|
*/
|
|
45
58
|
export declare function getT<const Ns extends readonly [Namespace, ...Namespace[]]>(locale: Locale | string, namespaces: Ns): Promise<MultiT<Ns>>;
|
|
46
59
|
export declare function getT<N extends Namespace>(locale: Locale | string, namespace: N): Promise<TFunction<N>>;
|
package/dist/server/index.js
CHANGED
|
@@ -2,42 +2,54 @@ import { createFormat } from '../runtime/format.js';
|
|
|
2
2
|
import { createT } from '../runtime/create-t.js';
|
|
3
3
|
import { getDir, getOgLocale, isLocale as isLocaleValue } from '../runtime/locale.js';
|
|
4
4
|
import { TongueError } from '../errors.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
let
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
const SERVER_STATE_KEY = '__nextTongueServerRuntime';
|
|
6
|
+
function serverState() {
|
|
7
|
+
const g = globalThis;
|
|
8
|
+
let s = g[SERVER_STATE_KEY];
|
|
9
|
+
if (!s) {
|
|
10
|
+
s = {
|
|
11
|
+
locales: [],
|
|
12
|
+
defaultLocale: 'en',
|
|
13
|
+
timeZone: 'UTC',
|
|
14
|
+
rootParams: false,
|
|
15
|
+
rootLocaleReader: undefined,
|
|
16
|
+
namespaceLoader: undefined,
|
|
17
|
+
};
|
|
18
|
+
g[SERVER_STATE_KEY] = s;
|
|
19
|
+
}
|
|
20
|
+
return s;
|
|
21
|
+
}
|
|
11
22
|
/** @internal Used by generated `.tongue/runtime.js` / tests. */
|
|
12
23
|
export function __setServerRuntime(opts) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
const s = serverState();
|
|
25
|
+
s.locales = opts.locales;
|
|
26
|
+
s.defaultLocale = opts.defaultLocale;
|
|
27
|
+
s.timeZone = opts.timeZone ?? 'UTC';
|
|
28
|
+
s.rootParams = opts.rootParams === true;
|
|
29
|
+
s.rootLocaleReader = opts.readRootLocale;
|
|
30
|
+
s.namespaceLoader = opts.loadNamespace;
|
|
19
31
|
}
|
|
20
32
|
/** Whether `routing.rootParams` is enabled after `ensureTongue()`. */
|
|
21
33
|
export function isRootParamsEnabled() {
|
|
22
|
-
return
|
|
34
|
+
return serverState().rootParams;
|
|
23
35
|
}
|
|
24
36
|
/** Configured locales after `ensureTongue()` / `__setServerRuntime`. */
|
|
25
37
|
export function getLocales() {
|
|
26
|
-
return
|
|
38
|
+
return serverState().locales;
|
|
27
39
|
}
|
|
28
40
|
export function getDefaultLocale() {
|
|
29
|
-
return
|
|
41
|
+
return serverState().defaultLocale;
|
|
30
42
|
}
|
|
31
43
|
/** @deprecated Prefer `getLocales()`; kept for docs that reference `.value`. */
|
|
32
44
|
export const locales = {
|
|
33
45
|
get value() {
|
|
34
|
-
return
|
|
46
|
+
return serverState().locales;
|
|
35
47
|
},
|
|
36
48
|
};
|
|
37
49
|
/** @deprecated Prefer `getDefaultLocale()`. */
|
|
38
50
|
export const defaultLocale = {
|
|
39
51
|
get value() {
|
|
40
|
-
return
|
|
52
|
+
return serverState().defaultLocale;
|
|
41
53
|
},
|
|
42
54
|
};
|
|
43
55
|
export async function getT(localeOrNs, namespace) {
|
|
@@ -57,7 +69,8 @@ export async function getT(localeOrNs, namespace) {
|
|
|
57
69
|
return getTWithLocale(String(localeOrNs), namespace);
|
|
58
70
|
}
|
|
59
71
|
const single = String(localeOrNs);
|
|
60
|
-
|
|
72
|
+
const s = serverState();
|
|
73
|
+
if (s.rootParams && s.locales.length && !s.locales.includes(single)) {
|
|
61
74
|
// Treat as namespace when it is not a configured locale tag.
|
|
62
75
|
const locale = await resolveRootLocale();
|
|
63
76
|
return getTWithLocale(locale, single);
|
|
@@ -68,10 +81,11 @@ export async function getT(localeOrNs, namespace) {
|
|
|
68
81
|
async function getTWithLocale(locale, ns) {
|
|
69
82
|
await ensureRuntime();
|
|
70
83
|
assertLocale(locale);
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
const loader = serverState().namespaceLoader;
|
|
85
|
+
if (!loader) {
|
|
86
|
+
throw new TongueError('TONGUE_E_TYPES_MISSING', 'Message modules not loaded. Wrap next.config with withTongue(), run `tongue build && next build`, and statically import `#tongue/runtime` in app/[locale]/layout (e.g. `import { localeParams } from \'#tongue/runtime\'`). package.json must map `"#tongue/*": "./.tongue/*.js"`.');
|
|
73
87
|
}
|
|
74
|
-
const messages = await
|
|
88
|
+
const messages = await loader(ns);
|
|
75
89
|
return createT({
|
|
76
90
|
locale,
|
|
77
91
|
messages,
|
|
@@ -82,10 +96,11 @@ async function getTWithLocale(locale, ns) {
|
|
|
82
96
|
}
|
|
83
97
|
/**
|
|
84
98
|
* Load `#tongue/runtime` and call `ensureTongue()` once when the server runtime
|
|
85
|
-
* is not yet wired
|
|
99
|
+
* is not yet wired. Prefer a static import in the [locale] layout; this lazy
|
|
100
|
+
* path uses globalThis so it shares state with that import.
|
|
86
101
|
*/
|
|
87
102
|
async function ensureRuntime() {
|
|
88
|
-
if (namespaceLoader)
|
|
103
|
+
if (serverState().namespaceLoader)
|
|
89
104
|
return;
|
|
90
105
|
try {
|
|
91
106
|
const importer = new Function('s', 'return import(s)');
|
|
@@ -98,14 +113,15 @@ async function ensureRuntime() {
|
|
|
98
113
|
}
|
|
99
114
|
export async function getFormat(locale, options) {
|
|
100
115
|
await ensureRuntime();
|
|
101
|
-
const
|
|
116
|
+
const s = serverState();
|
|
117
|
+
const resolved = locale ?? (s.rootParams ? await resolveRootLocale() : undefined);
|
|
102
118
|
if (resolved == null) {
|
|
103
119
|
throw new TongueError('TONGUE_E_LOCALE', 'getFormat(locale) requires a locale, or routing.rootParams: true');
|
|
104
120
|
}
|
|
105
121
|
assertLocale(resolved);
|
|
106
122
|
return createFormat({
|
|
107
123
|
locale: resolved,
|
|
108
|
-
timeZone: options?.timeZone ??
|
|
124
|
+
timeZone: options?.timeZone ?? s.timeZone,
|
|
109
125
|
});
|
|
110
126
|
}
|
|
111
127
|
/**
|
|
@@ -117,8 +133,9 @@ async function importRootParams() {
|
|
|
117
133
|
return importer('next/root-params');
|
|
118
134
|
}
|
|
119
135
|
async function resolveRootLocale() {
|
|
120
|
-
|
|
121
|
-
|
|
136
|
+
const reader = serverState().rootLocaleReader;
|
|
137
|
+
if (reader)
|
|
138
|
+
return reader();
|
|
122
139
|
try {
|
|
123
140
|
const mod = await importRootParams();
|
|
124
141
|
if (typeof mod.locale !== 'function') {
|
|
@@ -140,15 +157,16 @@ async function resolveRootLocale() {
|
|
|
140
157
|
export function isLocale(value) {
|
|
141
158
|
if (typeof value !== 'string' || value === '')
|
|
142
159
|
return false;
|
|
160
|
+
const configured = serverState().locales;
|
|
143
161
|
// Match assertLocale: empty allow-list means runtime not wired yet.
|
|
144
|
-
if (!
|
|
162
|
+
if (!configured.length) {
|
|
145
163
|
return !/[\0\r\n\\/]/.test(value) && !value.includes('..');
|
|
146
164
|
}
|
|
147
|
-
return isLocaleValue(value,
|
|
165
|
+
return isLocaleValue(value, configured);
|
|
148
166
|
}
|
|
149
167
|
/** Use as `export const generateStaticParams = localeParams` on `app/[locale]/layout`. */
|
|
150
168
|
export function localeParams() {
|
|
151
|
-
return
|
|
169
|
+
return serverState().locales.map((locale) => ({ locale }));
|
|
152
170
|
}
|
|
153
171
|
export function createTranslator(locale) {
|
|
154
172
|
assertLocale(locale);
|
|
@@ -173,7 +191,8 @@ export { isSanctionedUnit, SIMPLE_UNITS } from '../runtime/format.js';
|
|
|
173
191
|
export { setLocalePreference, } from './locale-preference.js';
|
|
174
192
|
export { requestLocale, LOCALE_COOKIE_NAME, LOCALE_HEADER_NAME, } from './request-locale.js';
|
|
175
193
|
function assertLocale(locale) {
|
|
176
|
-
|
|
177
|
-
|
|
194
|
+
const configured = serverState().locales;
|
|
195
|
+
if (configured.length && !configured.includes(locale)) {
|
|
196
|
+
throw new TongueError('TONGUE_E_LOCALE', `Locale "${locale}" is not in configured locales [${configured.join(', ')}]`);
|
|
178
197
|
}
|
|
179
198
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-tongue",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.16",
|
|
4
4
|
"description": "Compiler-first, type-safe i18n for the Next.js 16 App Router",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -76,6 +76,11 @@
|
|
|
76
76
|
"import": "./dist/navigation/index.js",
|
|
77
77
|
"default": "./dist/navigation/index.js"
|
|
78
78
|
},
|
|
79
|
+
"./navigation/server": {
|
|
80
|
+
"types": "./dist/navigation/server.d.ts",
|
|
81
|
+
"import": "./dist/navigation/server.js",
|
|
82
|
+
"default": "./dist/navigation/server.js"
|
|
83
|
+
},
|
|
79
84
|
"./testing": {
|
|
80
85
|
"types": "./dist/testing/index.d.ts",
|
|
81
86
|
"import": "./dist/testing/index.js",
|