next-tongue 0.9.14 → 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 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
- pkg.imports = { ...(pkg.imports ?? {}), '#tongue/*': './.tongue/*' };
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
- // Importing #tongue/runtime auto-runs ensureTongue() (withTongue apps).
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
- // Importing #tongue/runtime auto-runs ensureTongue() (withTongue apps).
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({
@@ -27,7 +27,9 @@ export function mergeProxySource(source, opts) {
27
27
  return {
28
28
  status: 'merged',
29
29
  code: ensureTrailingNewline(replaced),
30
- note: `Replaced final NextResponse.next() with ${tongueIdent}(${reqIdent}). Early next() returns (allow-lists) are kept.`,
30
+ note: `Replaced final NextResponse.next() with ${tongueIdent}(${reqIdent}). ` +
31
+ `Early next() returns (allow-lists) are kept. ` +
32
+ `If you stamp CSP/nonce on next(), use createProxy({ wrapResponse }) or mergeProxyResponse — see routing docs.`,
31
33
  };
32
34
  }
33
35
  // No next() — wrap exported handler so user logic runs first, then tongue.
@@ -36,7 +38,9 @@ export function mergeProxySource(source, opts) {
36
38
  return {
37
39
  status: 'merged',
38
40
  code: ensureTrailingNewline(wrapped),
39
- note: `Wrapped existing handler: user Response wins unless it is a passthrough next; otherwise ${tongueIdent} runs.`,
41
+ note: `Wrapped existing handler: non-passthrough user Response wins (auth/maintenance); ` +
42
+ `passthrough next() → ${tongueIdent} with mergeProxyResponse (keeps CSP/nonce). ` +
43
+ `Rewrites to paths under [locale] must include the locale segment.`,
40
44
  };
41
45
  }
42
46
  return {
@@ -117,14 +121,50 @@ function wrapExportedHandler(code, tongueIdent, reqIdent) {
117
121
  const name = m[2];
118
122
  const inner = `__tongue_user_${name}`;
119
123
  let out = code.slice(0, m.index) + `${asyncKw}function ${inner}(` + code.slice(m.index + m[0].length);
124
+ // Prefer non-passthrough user Response (auth redirect, maintenance rewrite).
125
+ // Passthrough next() → tongue, then mergeProxyResponse so CSP/nonce on the
126
+ // user next() are not dropped.
127
+ out = ensureMergeProxyResponseImport(out);
128
+ // Outer wrapper is always async — createProxy returns a Promise.
120
129
  out +=
121
- `\n\nexport ${asyncKw}function ${name}(${reqIdent}: Parameters<typeof ${inner}>[0]) {\n` +
130
+ `\n\nexport async function ${name}(${reqIdent}: Parameters<typeof ${inner}>[0]) {\n` +
122
131
  ` const __user = ${asyncKw ? 'await ' : ''}${inner}(${reqIdent});\n` +
123
132
  ` if (__user && __user.headers?.get?.('x-middleware-next') !== '1') return __user;\n` +
124
- ` return ${tongueIdent}(${reqIdent});\n` +
133
+ ` const __tongue = await ${tongueIdent}(${reqIdent});\n` +
134
+ ` return __user ? mergeProxyResponse(__user, __tongue) : __tongue;\n` +
125
135
  `}\n`;
126
136
  return out;
127
137
  }
138
+ function ensureMergeProxyResponseImport(code) {
139
+ if (/\bmergeProxyResponse\b/.test(code) && CREATE_PROXY_IMPORT.test(code)) {
140
+ return code.replace(CREATE_PROXY_IMPORT, (full, specs) => {
141
+ if (/\bmergeProxyResponse\b/.test(specs))
142
+ return full;
143
+ const parts = specs
144
+ .split(',')
145
+ .map((s) => s.trim())
146
+ .filter(Boolean);
147
+ parts.push('mergeProxyResponse');
148
+ return full.replace(specs, ` ${parts.join(', ')} `);
149
+ });
150
+ }
151
+ // ensureCreateProxyImport already ran — add mergeProxyResponse to that import
152
+ if (CREATE_PROXY_IMPORT.test(code)) {
153
+ return code.replace(CREATE_PROXY_IMPORT, (full, specs) => {
154
+ if (/\bmergeProxyResponse\b/.test(specs))
155
+ return full;
156
+ const parts = specs
157
+ .split(',')
158
+ .map((s) => s.trim())
159
+ .filter(Boolean);
160
+ if (!parts.includes('createProxy'))
161
+ parts.unshift('createProxy');
162
+ parts.push('mergeProxyResponse');
163
+ return full.replace(specs, ` ${parts.join(', ')} `);
164
+ });
165
+ }
166
+ return code;
167
+ }
128
168
  export const PROXY_CANDIDATES = [
129
169
  'proxy.ts',
130
170
  'proxy.js',
@@ -39,5 +39,7 @@ export declare const defaultLocale: {
39
39
  export type { TFunction, Format };
40
40
  export type { SanctionedUnit, SimpleUnit, DisplayNameType, } from '../runtime/format.js';
41
41
  export { isSanctionedUnit, SIMPLE_UNITS } from '../runtime/format.js';
42
+ /** Isomorphic — safe in client HtmlLang helpers. */
43
+ export { getDir } from '../runtime/locale.js';
42
44
  export { translateIssues, defaultFormatPath, } from '../runtime/translate-issues.js';
43
45
  export type { TranslateIssue, TranslateIssuesOptions, IssuePathSegment, } from '../runtime/translate-issues.js';
@@ -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('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: [...] }) with an inline array or a same-file const (imported from arrays are not rewritten).`);
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()'),
@@ -116,4 +116,6 @@ export const defaultLocale = {
116
116
  },
117
117
  };
118
118
  export { isSanctionedUnit, SIMPLE_UNITS } from '../runtime/format.js';
119
+ /** Isomorphic — safe in client HtmlLang helpers. */
120
+ export { getDir } from '../runtime/locale.js';
119
121
  export { translateIssues, defaultFormatPath, } from '../runtime/translate-issues.js';
@@ -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. Map `#tongue/*` → this directory.\nImporting `#tongue/runtime` (or calling getT) auto-runs ensureTongue(); an explicit call is optional.\n', 'utf8');
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);
@@ -14,6 +14,7 @@ export function emitTypesFile(opts) {
14
14
  : 'string';
15
15
  return `/* eslint-disable */
16
16
  // Generated by next-tongue. Do not edit.
17
+ import type { ReactNode } from 'react';
17
18
  declare module 'next-tongue' {
18
19
  interface TongueRegister {
19
20
  locales: ${localeUnion};
@@ -46,8 +47,8 @@ function argsToType(args) {
46
47
  }
47
48
  case 'rich':
48
49
  return kind.selfClosing
49
- ? `${JSON.stringify(name)}: () => unknown`
50
- : `${JSON.stringify(name)}: (chunks: unknown) => unknown`;
50
+ ? `${JSON.stringify(name)}: () => ReactNode`
51
+ : `${JSON.stringify(name)}: (chunks: ReactNode) => ReactNode`;
51
52
  }
52
53
  });
53
54
  return `{ ${fields.join('; ')} }`;
@@ -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
- return extractStringLiterals(arr.slice(1, -1));
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 declRe = /(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?(useT|getT)\s*\(\s*(?:[^'"\n,]+,\s*)?(['"])([^'"]*)\3\s*\)/g;
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: m[4] || '_root',
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,6 @@
1
+ export type NavigationConfig = {
2
+ locales: readonly string[];
3
+ defaultLocale: string;
4
+ prefix?: 'always' | 'as-needed' | 'never';
5
+ pathnames?: Record<string, Partial<Record<string, string>>>;
6
+ };
@@ -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.
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { NavigationConfig } from './index.js';
2
+ import type { NavigationConfig } from './config.js';
3
3
  export type LocaleSwitcherOption = {
4
4
  locale: string;
5
5
  /** Locale name in that locale (`Intl.DisplayNames`). */
@@ -2,6 +2,8 @@ export type PathnameInput = string | {
2
2
  pathname: string;
3
3
  params?: Record<string, string | number>;
4
4
  query?: Record<string, string | number | undefined>;
5
+ /** Fragment without requiring `#` (e.g. `'preferences'` or `'#preferences'`). */
6
+ hash?: string;
5
7
  };
6
8
  export type GetPathnameArgs = {
7
9
  href: PathnameInput;
@@ -15,6 +17,9 @@ export type GetPathnameArgs = {
15
17
  };
16
18
  /**
17
19
  * Build a localized public pathname for links, redirects, and sitemaps.
20
+ * Query strings and hashes are preserved (stripped before pathnames/prefix,
21
+ * reattached after). Prefer the object form for typed `pathnames` unions:
22
+ * `{ pathname: '/dashboard', query: { demo: 1 }, hash: 'prefs' }`.
18
23
  */
19
24
  export declare function getPathname(args: GetPathnameArgs): string;
20
25
  /** Strip a leading locale segment from a pathname. */
@@ -24,3 +29,9 @@ export declare function stripLocalePrefix(pathname: string, locales: readonly st
24
29
  * used in `routing.pathnames` (e.g. `/ueber-uns` → `/about` for `de`).
25
30
  */
26
31
  export declare function toInternalPathname(pathname: string, locale: string, pathnames?: Record<string, Partial<Record<string, string>>>): string;
32
+ /** Split `/path?q=1#hash` into parts (pathnames lookup needs the path alone). */
33
+ export declare function splitPathSearchHash(full: string): {
34
+ pathname: string;
35
+ search: string;
36
+ hash: string;
37
+ };
@@ -1,23 +1,29 @@
1
1
  /**
2
2
  * Build a localized public pathname for links, redirects, and sitemaps.
3
+ * Query strings and hashes are preserved (stripped before pathnames/prefix,
4
+ * reattached after). Prefer the object form for typed `pathnames` unions:
5
+ * `{ pathname: '/dashboard', query: { demo: 1 }, hash: 'prefs' }`.
3
6
  */
4
7
  export function getPathname(args) {
5
8
  const { href, locale, prefix = 'as-needed', defaultLocale = 'en', pathnames, } = args;
6
- let path = resolveHref(href);
9
+ const { pathname: rawPath, search, hash } = resolveHref(href);
10
+ let path = rawPath;
7
11
  if (pathnames) {
8
12
  const localized = pathnames[path]?.[locale];
9
13
  if (localized)
10
14
  path = localized;
11
15
  }
12
- if (prefix === 'never')
13
- return path;
16
+ if (prefix === 'never') {
17
+ return path + search + hash;
18
+ }
14
19
  if (prefix === 'as-needed' && locale === defaultLocale) {
15
- return path;
20
+ return path + search + hash;
16
21
  }
17
22
  // always, or as-needed for non-default
18
- if (path === '/')
19
- return `/${locale}`;
20
- return `/${locale}${path.startsWith('/') ? path : `/${path}`}`;
23
+ if (path === '/') {
24
+ return `/${locale}` + search + hash;
25
+ }
26
+ return (`/${locale}${path.startsWith('/') ? path : `/${path}`}` + search + hash);
21
27
  }
22
28
  /** Strip a leading locale segment from a pathname. */
23
29
  export function stripLocalePrefix(pathname, locales) {
@@ -43,14 +49,21 @@ export function toInternalPathname(pathname, locale, pathnames) {
43
49
  return pathname;
44
50
  }
45
51
  function resolveHref(href) {
46
- if (typeof href === 'string')
47
- return href.startsWith('/') ? href : `/${href}`;
48
- let path = href.pathname.startsWith('/') ? href.pathname : `/${href.pathname}`;
52
+ if (typeof href === 'string') {
53
+ return splitPathSearchHash(href.startsWith('/') ? href : `/${href}`);
54
+ }
55
+ let path = href.pathname.startsWith('/')
56
+ ? href.pathname
57
+ : `/${href.pathname}`;
58
+ // Object pathname should be path-only; tolerate accidental ?/#.
59
+ const split = splitPathSearchHash(path);
60
+ path = split.pathname;
49
61
  if (href.params) {
50
62
  for (const [key, value] of Object.entries(href.params)) {
51
63
  path = path.replace(`[${key}]`, encodeURIComponent(String(value)));
52
64
  }
53
65
  }
66
+ let search = split.search;
54
67
  if (href.query) {
55
68
  const qs = new URLSearchParams();
56
69
  for (const [key, value] of Object.entries(href.query)) {
@@ -58,8 +71,28 @@ function resolveHref(href) {
58
71
  qs.set(key, String(value));
59
72
  }
60
73
  const s = qs.toString();
61
- if (s)
62
- path += `?${s}`;
74
+ search = s ? `?${s}` : '';
75
+ }
76
+ let hash = split.hash;
77
+ if (href.hash != null && href.hash !== '') {
78
+ hash = href.hash.startsWith('#') ? href.hash : `#${href.hash}`;
79
+ }
80
+ return { pathname: path, search, hash };
81
+ }
82
+ /** Split `/path?q=1#hash` into parts (pathnames lookup needs the path alone). */
83
+ export function splitPathSearchHash(full) {
84
+ let path = full;
85
+ let hash = '';
86
+ let search = '';
87
+ const hashIdx = path.indexOf('#');
88
+ if (hashIdx >= 0) {
89
+ hash = path.slice(hashIdx);
90
+ path = path.slice(0, hashIdx);
91
+ }
92
+ const qIdx = path.indexOf('?');
93
+ if (qIdx >= 0) {
94
+ search = path.slice(qIdx);
95
+ path = path.slice(0, qIdx);
63
96
  }
64
- return path;
97
+ return { pathname: path || '/', search, hash };
65
98
  }
@@ -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
+ }
@@ -47,6 +47,12 @@ export type CreateProxyOptions = {
47
47
  /** Override detection (domain → cookie → Accept-Language → default). */
48
48
  resolveLocale?: (req: TongueProxyRequest) => string | undefined | Promise<string | undefined>;
49
49
  onLocale?: (locale: string, req: TongueProxyRequest) => void;
50
+ /**
51
+ * Post-process every tongue response (redirect / rewrite / next).
52
+ * Use to re-stamp CSP / nonce headers after locale routing — tongue does
53
+ * not preserve headers from a prior `NextResponse.next()`.
54
+ */
55
+ wrapResponse?: (res: Response, req: TongueProxyRequest) => Response | Promise<Response>;
50
56
  };
51
57
  /** Default locale cookie name (`SameSite=Lax`). Use with `requestLocale` in API routes. */
52
58
  export declare const LOCALE_COOKIE_NAME = "TONGUE_LOCALE";
@@ -57,6 +63,15 @@ export declare const LOCALE_COOKIE_NAME = "TONGUE_LOCALE";
57
63
  * @see docs/routing-and-proxy.md
58
64
  */
59
65
  export declare function createProxy(options: CreateProxyOptions): (req: TongueProxyRequest) => Response | Promise<Response>;
66
+ /**
67
+ * Copy response headers from a prior `NextResponse.next()` (CSP, nonce, …)
68
+ * onto a tongue rewrite/redirect/next response. Skips middleware control
69
+ * headers (`x-middleware-*`, `Location` when tongue already set one).
70
+ *
71
+ * Use when you stamp headers on `next()` then call tongue — tongue alone
72
+ * cannot see those headers.
73
+ */
74
+ export declare function mergeProxyResponse(prior: Response, tongue: Response): Response;
60
75
  /** Map internal path → localized public path for a locale. */
61
76
  export declare function publicPathForLocale(internal: string, locale: string, pathnames?: Record<string, Partial<Record<string, string>>>): string;
62
77
  /** Resolve `Host` / `x-forwarded-host` against `routing.domains`. */
@@ -37,78 +37,125 @@ export function createProxy(options) {
37
37
  if (!localeSet.has(defaultLocale)) {
38
38
  throw new Error(`[next-tongue] createProxy: defaultLocale "${defaultLocale}" is not in locales`);
39
39
  }
40
+ const wrap = options.wrapResponse;
40
41
  return async (req) => {
41
- const { pathname } = req.nextUrl;
42
- const https = isHttpsRequest(req);
43
- if (isUnsafePath(pathname)) {
44
- return passthrough(cookieCfg, null, https);
45
- }
46
- const domain = matchDomain(req, domains);
47
- const activeLocales = domain?.locales ? [...domain.locales] : locales;
48
- const activeDefault = domain?.defaultLocale ?? defaultLocale;
49
- const activeSet = new Set(activeLocales);
50
- const activeMatcher = activeLocales.length === locales.length
51
- ? detectionMatcher
52
- : createLocaleMatcher(activeLocales.filter((l) => !excludeDetection.has(l)));
53
- const matched = matchLocalePrefix(pathname, localeSet);
54
- if (matched && (!domain || activeSet.has(matched.locale))) {
55
- const { locale, rest } = matched;
56
- const publicRest = rest === '' ? '/' : rest;
57
- const internal = toInternalPathname(publicRest, locale, pathnames);
58
- if (prefix === 'as-needed' && locale === activeDefault) {
59
- // Canonicalize default-locale URLs without the prefix.
60
- const dest = publicPathForLocale(internal, locale, pathnames);
61
- const redirectUrl = req.nextUrl.clone();
62
- redirectUrl.pathname = dest;
63
- options.onLocale?.(locale, req);
64
- return redirectResponse(redirectUrl.toString(), 308, cookieCfg, locale, https);
65
- }
42
+ const res = await handleProxyRequest(req, {
43
+ locales,
44
+ defaultLocale,
45
+ prefix,
46
+ localeDetection,
47
+ pathnames,
48
+ domains,
49
+ cookieCfg,
50
+ excludeDetection,
51
+ localeSet,
52
+ detectionMatcher,
53
+ options,
54
+ });
55
+ return wrap ? await wrap(res, req) : res;
56
+ };
57
+ }
58
+ async function handleProxyRequest(req, ctx) {
59
+ const { locales, defaultLocale, prefix, localeDetection, pathnames, domains, cookieCfg, excludeDetection, localeSet, detectionMatcher, options, } = ctx;
60
+ const { pathname } = req.nextUrl;
61
+ const https = isHttpsRequest(req);
62
+ if (isUnsafePath(pathname)) {
63
+ return passthrough(cookieCfg, null, https);
64
+ }
65
+ const domain = matchDomain(req, domains);
66
+ const activeLocales = domain?.locales ? [...domain.locales] : locales;
67
+ const activeDefault = domain?.defaultLocale ?? defaultLocale;
68
+ const activeSet = new Set(activeLocales);
69
+ const activeMatcher = activeLocales.length === locales.length
70
+ ? detectionMatcher
71
+ : createLocaleMatcher(activeLocales.filter((l) => !excludeDetection.has(l)));
72
+ const matched = matchLocalePrefix(pathname, localeSet);
73
+ if (matched && (!domain || activeSet.has(matched.locale))) {
74
+ const { locale, rest } = matched;
75
+ const publicRest = rest === '' ? '/' : rest;
76
+ const internal = toInternalPathname(publicRest, locale, pathnames);
77
+ if (prefix === 'as-needed' && locale === activeDefault) {
78
+ // Canonicalize default-locale URLs without the prefix.
79
+ const dest = publicPathForLocale(internal, locale, pathnames);
80
+ const redirectUrl = req.nextUrl.clone();
81
+ redirectUrl.pathname = dest;
66
82
  options.onLocale?.(locale, req);
67
- if (internal !== publicRest) {
68
- const url = req.nextUrl.clone();
69
- url.pathname = joinLocalePath(locale, internal);
70
- return rewriteResponse(url.toString(), cookieCfg, locale, https);
71
- }
72
- return passthrough(cookieCfg, locale, https);
83
+ return redirectResponse(redirectUrl.toString(), 308, cookieCfg, locale, https);
73
84
  }
74
- const resolved = options.resolveLocale
75
- ? await options.resolveLocale(req)
76
- : undefined;
77
- let locale = sanitizeLocale(resolved, activeLocales) ??
78
- (localeDetection
79
- ? detectLocale(req, activeLocales, activeDefault, cookieCfg, excludeDetection, activeMatcher, domain)
80
- : activeDefault);
81
- locale = sanitizeLocale(locale, activeLocales) ?? activeDefault;
82
85
  options.onLocale?.(locale, req);
83
- const internal = toInternalPathname(pathname, locale, pathnames);
84
- // Prefer localized public path when the user hit the internal key.
85
- const publicPath = publicPathForLocale(internal, locale, pathnames);
86
- if (prefix === 'never') {
86
+ if (internal !== publicRest) {
87
87
  const url = req.nextUrl.clone();
88
88
  url.pathname = joinLocalePath(locale, internal);
89
89
  return rewriteResponse(url.toString(), cookieCfg, locale, https);
90
90
  }
91
- if (prefix === 'as-needed' && locale === activeDefault) {
92
- // Serve default locale without prefix; rewrite to internal app path.
93
- if (pathname !== publicPath && pathnames) {
94
- // User visited internal `/about` but public is also `/about` — fine.
95
- // If they somehow differ for default locale, redirect to public.
96
- if (publicPath !== internal && pathname === internal) {
97
- const redirectUrl = req.nextUrl.clone();
98
- redirectUrl.pathname = publicPath;
99
- return redirectResponse(redirectUrl.toString(), 308, cookieCfg, locale, https);
100
- }
91
+ return passthrough(cookieCfg, locale, https);
92
+ }
93
+ const resolved = options.resolveLocale
94
+ ? await options.resolveLocale(req)
95
+ : undefined;
96
+ let locale = sanitizeLocale(resolved, activeLocales) ??
97
+ (localeDetection
98
+ ? detectLocale(req, activeLocales, activeDefault, cookieCfg, excludeDetection, activeMatcher, domain)
99
+ : activeDefault);
100
+ locale = sanitizeLocale(locale, activeLocales) ?? activeDefault;
101
+ options.onLocale?.(locale, req);
102
+ const internal = toInternalPathname(pathname, locale, pathnames);
103
+ // Prefer localized public path when the user hit the internal key.
104
+ const publicPath = publicPathForLocale(internal, locale, pathnames);
105
+ if (prefix === 'never') {
106
+ const url = req.nextUrl.clone();
107
+ url.pathname = joinLocalePath(locale, internal);
108
+ return rewriteResponse(url.toString(), cookieCfg, locale, https);
109
+ }
110
+ if (prefix === 'as-needed' && locale === activeDefault) {
111
+ // Serve default locale without prefix; rewrite to internal app path.
112
+ if (pathname !== publicPath && pathnames) {
113
+ // User visited internal `/about` but public is also `/about` — fine.
114
+ // If they somehow differ for default locale, redirect to public.
115
+ if (publicPath !== internal && pathname === internal) {
116
+ const redirectUrl = req.nextUrl.clone();
117
+ redirectUrl.pathname = publicPath;
118
+ return redirectResponse(redirectUrl.toString(), 308, cookieCfg, locale, https);
101
119
  }
102
- const url = req.nextUrl.clone();
103
- url.pathname = joinLocalePath(activeDefault, internal);
104
- return rewriteResponse(url.toString(), cookieCfg, locale, https);
105
120
  }
106
- // Non-default (or always): redirect to prefixed localized public URL.
107
- const destPublic = publicPathForLocale(internal, locale, pathnames);
108
121
  const url = req.nextUrl.clone();
109
- url.pathname = joinLocalePath(locale, destPublic);
110
- return redirectResponse(url.toString(), 307, cookieCfg, locale, https);
111
- };
122
+ url.pathname = joinLocalePath(activeDefault, internal);
123
+ return rewriteResponse(url.toString(), cookieCfg, locale, https);
124
+ }
125
+ // Non-default (or always): redirect to prefixed localized public URL.
126
+ const destPublic = publicPathForLocale(internal, locale, pathnames);
127
+ const url = req.nextUrl.clone();
128
+ url.pathname = joinLocalePath(locale, destPublic);
129
+ return redirectResponse(url.toString(), 307, cookieCfg, locale, https);
130
+ }
131
+ /**
132
+ * Copy response headers from a prior `NextResponse.next()` (CSP, nonce, …)
133
+ * onto a tongue rewrite/redirect/next response. Skips middleware control
134
+ * headers (`x-middleware-*`, `Location` when tongue already set one).
135
+ *
136
+ * Use when you stamp headers on `next()` then call tongue — tongue alone
137
+ * cannot see those headers.
138
+ */
139
+ export function mergeProxyResponse(prior, tongue) {
140
+ const headers = new Headers(tongue.headers);
141
+ prior.headers.forEach((value, key) => {
142
+ const k = key.toLowerCase();
143
+ if (k.startsWith('x-middleware-'))
144
+ return;
145
+ if (k === 'location' && headers.has('location'))
146
+ return;
147
+ if (k === 'set-cookie') {
148
+ headers.append(key, value);
149
+ return;
150
+ }
151
+ if (!headers.has(key))
152
+ headers.set(key, value);
153
+ });
154
+ return new Response(tongue.body, {
155
+ status: tongue.status,
156
+ statusText: tongue.statusText,
157
+ headers,
158
+ });
112
159
  }
113
160
  /** Map internal path → localized public path for a locale. */
114
161
  export function publicPathForLocale(internal, locale, pathnames) {
@@ -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
- declare let namespaceLoader: ((namespace: string) => Promise<Record<string, MessageFn>>) | undefined;
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?: typeof namespaceLoader;
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
- * `withTongue()` + generated `#tongue/runtime` auto-wires messages; an explicit
43
- * `ensureTongue()` call is optional.
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>>;
@@ -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
- let runtimeLocales = [];
6
- let runtimeDefaultLocale = 'en';
7
- let runtimeTimeZone = 'UTC';
8
- let runtimeRootParams = false;
9
- let rootLocaleReader;
10
- let namespaceLoader;
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
- runtimeLocales = opts.locales;
14
- runtimeDefaultLocale = opts.defaultLocale;
15
- runtimeTimeZone = opts.timeZone ?? 'UTC';
16
- runtimeRootParams = opts.rootParams === true;
17
- rootLocaleReader = opts.readRootLocale;
18
- namespaceLoader = opts.loadNamespace;
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 runtimeRootParams;
34
+ return serverState().rootParams;
23
35
  }
24
36
  /** Configured locales after `ensureTongue()` / `__setServerRuntime`. */
25
37
  export function getLocales() {
26
- return runtimeLocales;
38
+ return serverState().locales;
27
39
  }
28
40
  export function getDefaultLocale() {
29
- return runtimeDefaultLocale;
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 runtimeLocales;
46
+ return serverState().locales;
35
47
  },
36
48
  };
37
49
  /** @deprecated Prefer `getDefaultLocale()`. */
38
50
  export const defaultLocale = {
39
51
  get value() {
40
- return runtimeDefaultLocale;
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
- if (runtimeRootParams && runtimeLocales.length && !runtimeLocales.includes(single)) {
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
- if (!namespaceLoader) {
72
- throw new TongueError('TONGUE_E_TYPES_MISSING', 'Message modules not loaded. Wrap next.config with withTongue() and run `tongue build && next build` so `#tongue/runtime` exists. (ensureTongue() is optional — getT loads it automatically.)');
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 namespaceLoader(ns);
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 (withTongue apps — no per-page ensureTongue boilerplate).
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 resolved = locale ?? (runtimeRootParams ? await resolveRootLocale() : undefined);
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 ?? runtimeTimeZone,
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
- if (rootLocaleReader)
121
- return rootLocaleReader();
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 (!runtimeLocales.length) {
162
+ if (!configured.length) {
145
163
  return !/[\0\r\n\\/]/.test(value) && !value.includes('..');
146
164
  }
147
- return isLocaleValue(value, runtimeLocales);
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 runtimeLocales.map((locale) => ({ locale }));
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
- if (runtimeLocales.length && !runtimeLocales.includes(locale)) {
177
- throw new TongueError('TONGUE_E_LOCALE', `Locale "${locale}" is not in configured locales [${runtimeLocales.join(', ')}]`);
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.14",
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",