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 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({
@@ -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()'),
@@ -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);
@@ -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`). */
@@ -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
+ }
@@ -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.15",
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",