@verbaly/next 0.19.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aron Soto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/AronSoto/verbaly/develop/assets/logo.png" alt="Verbaly" width="300" />
3
+ </p>
4
+
5
+ <p align="center"><em>Next.js integration for Verbaly — App Router/RSC with per-request locale negotiation, Turbopack and webpack support, flash-free hydration.</em></p>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@verbaly/next"><img src="https://img.shields.io/npm/v/@verbaly/next?logo=npm&color=cb3837" alt="npm version" /></a>
9
+ <a href="https://github.com/AronSoto/verbaly/blob/develop/LICENSE"><img src="https://img.shields.io/npm/l/@verbaly/next?color=blue" alt="MIT" /></a>
10
+ </p>
11
+
12
+ ---
13
+
14
+ Server Components render already translated in the visitor's language — cookie first, then `Accept-Language`, then your fallback — and Client Components hydrate with the **same locale and the same catalog**: no flash of untranslated text, no hydration mismatch. Each request gets its **own instance** (React `cache()`): no locale leaking between concurrent users.
15
+
16
+ Works with **Turbopack** (the Next 16 default) and webpack: the config wrapper generates the runtime module as real files and wires the `t`-template compiler as a loader for both.
17
+
18
+ ## 🚀 Install
19
+
20
+ ```bash
21
+ pnpm add verbaly @verbaly/next @verbaly/react
22
+ ```
23
+
24
+ ## ⚡ Wire it up
25
+
26
+ **1. The config wrapper** — this is the whole build setup:
27
+
28
+ ```ts
29
+ // next.config.ts
30
+ import { withVerbaly } from '@verbaly/next';
31
+
32
+ export default withVerbaly({
33
+ /* your Next config */
34
+ });
35
+ ```
36
+
37
+ Locales live in your `verbaly.config` (created by `npx verbaly init`), or inline:
38
+
39
+ ```ts
40
+ export default withVerbaly({}, { locales: ['en', 'es', 'pt'] });
41
+ ```
42
+
43
+ **2. The provider** — root layout, Server Component:
44
+
45
+ ```tsx
46
+ // app/layout.tsx
47
+ import { getRequestLocale, getVerbalyProps } from '@verbaly/next/server';
48
+ import { VerbalyProvider } from '@verbaly/next/client';
49
+
50
+ export default async function RootLayout({ children }: { children: React.ReactNode }) {
51
+ const locale = await getRequestLocale();
52
+ const props = await getVerbalyProps();
53
+ return (
54
+ <html lang={locale}>
55
+ <body>
56
+ <VerbalyProvider {...props}>{children}</VerbalyProvider>
57
+ </body>
58
+ </html>
59
+ );
60
+ }
61
+ ```
62
+
63
+ **3. Write text** — Server Components use `getT()`:
64
+
65
+ ```tsx
66
+ import { getT } from '@verbaly/next/server';
67
+
68
+ export default async function Page() {
69
+ const t = await getT();
70
+ return <h1>{t`Welcome back`}</h1>;
71
+ }
72
+ ```
73
+
74
+ Client Components use the React bindings (re-exported from `@verbaly/next/client`):
75
+
76
+ ```tsx
77
+ 'use client';
78
+ import { useT } from '@verbaly/next/client';
79
+
80
+ export function Counter() {
81
+ const t = useT();
82
+ return <p>{t`You have new messages`}</p>;
83
+ }
84
+ ```
85
+
86
+ **4. Switching languages** — persists the cookie the server reads and re-renders Server Components:
87
+
88
+ ```tsx
89
+ 'use client';
90
+ import { useSwitchLocale } from '@verbaly/next/client';
91
+
92
+ export function LocalePicker() {
93
+ const switchLocale = useSwitchLocale();
94
+ return <button onClick={() => void switchLocale('es')}>Español</button>;
95
+ }
96
+ ```
97
+
98
+ That's it. `next dev` extracts your messages live (catalogs + `verbaly.d.ts` stay fresh); `next build` blocks on missing translations.
99
+
100
+ ## 📖 Options
101
+
102
+ Second argument of `withVerbaly` — every [`verbaly.config`](https://www.npmjs.com/package/@verbaly/compiler) option, plus:
103
+
104
+ | Option | What it does |
105
+ | --- | --- |
106
+ | `cookie` | Cookie read/written for the user's choice (default `verbaly-locale`); `false` = `Accept-Language` only. |
107
+ | `fallback` | Locale when nothing matches (defaults to the source locale). |
108
+ | `failOnMissing` | `false` opts out of the build gate. |
109
+
110
+ - Negotiation reads `headers()`/`cookies()`, so negotiated routes render dynamically — for fully static output use [`verbaly render`](https://www.npmjs.com/package/@verbaly/compiler) per locale.
111
+ - The generated `.verbaly/` directory is build output (it ships its own `.gitignore`).
112
+
113
+ ## 📚 Docs
114
+
115
+ Full guide: [verbaly-web.vercel.app/docs](https://verbaly-web.vercel.app/docs)
116
+
117
+ ## License
118
+
119
+ [MIT](https://github.com/AronSoto/verbaly/blob/develop/LICENSE) © Aron Soto
@@ -0,0 +1,14 @@
1
+ import { ReactElement, ReactNode } from "react";
2
+ import { SwitchLocaleOptions } from "verbaly";
3
+ import { Trans, useLocale, useT, useVerbaly } from "@verbaly/react";
4
+ //#region src/client.d.ts
5
+ interface VerbalyProviderProps {
6
+ locale: string;
7
+ messages?: Record<string, string>;
8
+ children?: ReactNode;
9
+ }
10
+ declare function VerbalyProvider(props: VerbalyProviderProps): ReactElement;
11
+ declare function useSwitchLocale(): (locale: string, options?: SwitchLocaleOptions) => Promise<void>;
12
+ //#endregion
13
+ export { Trans, VerbalyProvider, VerbalyProviderProps, useLocale, useSwitchLocale, useT, useVerbaly };
14
+ //# sourceMappingURL=client.d.ts.map
package/dist/client.js ADDED
@@ -0,0 +1,41 @@
1
+ "use client";
2
+ import { createElement, useCallback, useEffect, useState } from "react";
3
+ import { switchLocale } from "verbaly";
4
+ import { createInstance, requestOptions, sourceLocale } from "virtual:verbaly";
5
+ import { useRouter } from "next/navigation.js";
6
+ import { Trans, VerbalyProvider as VerbalyProvider$1, useLocale, useT, useVerbaly, useVerbaly as useVerbaly$1 } from "@verbaly/react";
7
+ //#region src/client.ts
8
+ function VerbalyProvider(props) {
9
+ const [instance] = useState(() => {
10
+ const created = createInstance({ locale: props.locale });
11
+ if (props.messages && props.locale !== sourceLocale) created.addMessages(props.locale, props.messages);
12
+ return created;
13
+ });
14
+ useEffect(() => {
15
+ if (props.locale === instance.locale) return;
16
+ if (props.messages) {
17
+ instance.addMessages(props.locale, props.messages);
18
+ instance.setLocale(props.locale);
19
+ } else instance.loadLocale(props.locale).then(() => instance.setLocale(props.locale));
20
+ }, [
21
+ props.locale,
22
+ props.messages,
23
+ instance
24
+ ]);
25
+ return createElement(VerbalyProvider$1, { instance }, props.children);
26
+ }
27
+ function useSwitchLocale() {
28
+ const instance = useVerbaly$1();
29
+ const router = useRouter();
30
+ return useCallback(async (locale, options) => {
31
+ await switchLocale(instance, locale, {
32
+ cookie: requestOptions?.cookie,
33
+ ...options
34
+ });
35
+ router.refresh();
36
+ }, [instance, router]);
37
+ }
38
+ //#endregion
39
+ export { Trans, VerbalyProvider, useLocale, useSwitchLocale, useT, useVerbaly };
40
+
41
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":["ReactVerbalyProvider","useVerbaly"],"sources":["../src/client.ts"],"sourcesContent":["'use client';\n\nimport { useRouter } from 'next/navigation';\nimport {\n createElement,\n useCallback,\n useEffect,\n useState,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport { VerbalyProvider as ReactVerbalyProvider, useVerbaly } from '@verbaly/react';\nimport { switchLocale, type SwitchLocaleOptions } from 'verbaly';\nimport { createInstance, requestOptions, sourceLocale } from 'virtual:verbaly';\n\nexport { Trans, useLocale, useT, useVerbaly } from '@verbaly/react';\n\nexport interface VerbalyProviderProps {\n locale: string;\n messages?: Record<string, string>;\n children?: ReactNode;\n}\n\n// client side of the boundary: instance seeded synchronously from the serialized\n// catalog → hydration renders exactly the server's output, no flash\nexport function VerbalyProvider(props: VerbalyProviderProps): ReactElement {\n const [instance] = useState(() => {\n const created = createInstance({ locale: props.locale });\n if (props.messages && props.locale !== sourceLocale) {\n created.addMessages(props.locale, props.messages);\n }\n return created;\n });\n\n // server changed the locale out-of-band (cookie edit + refresh) — follow it\n useEffect(() => {\n if (props.locale === instance.locale) return;\n if (props.messages) {\n instance.addMessages(props.locale, props.messages);\n instance.setLocale(props.locale);\n } else {\n void instance.loadLocale(props.locale).then(() => instance.setLocale(props.locale));\n }\n }, [props.locale, props.messages, instance]);\n\n return createElement(ReactVerbalyProvider, { instance }, props.children);\n}\n\n// core switchLocale (catalog → locale → cookie + <html lang>) + RSC re-render\nexport function useSwitchLocale(): (\n locale: string,\n options?: SwitchLocaleOptions,\n) => Promise<void> {\n const instance = useVerbaly();\n const router = useRouter();\n return useCallback(\n async (locale, options) => {\n await switchLocale(instance, locale, { cookie: requestOptions?.cookie, ...options });\n router.refresh();\n },\n [instance, router],\n );\n}\n"],"mappings":";;;;;;;AAyBA,SAAgB,gBAAgB,OAA2C;CACzE,MAAM,CAAC,YAAY,eAAe;EAChC,MAAM,UAAU,eAAe,EAAE,QAAQ,MAAM,OAAO,CAAC;EACvD,IAAI,MAAM,YAAY,MAAM,WAAW,cACrC,QAAQ,YAAY,MAAM,QAAQ,MAAM,QAAQ;EAElD,OAAO;CACT,CAAC;CAGD,gBAAgB;EACd,IAAI,MAAM,WAAW,SAAS,QAAQ;EACtC,IAAI,MAAM,UAAU;GAClB,SAAS,YAAY,MAAM,QAAQ,MAAM,QAAQ;GACjD,SAAS,UAAU,MAAM,MAAM;EACjC,OACE,SAAc,WAAW,MAAM,MAAM,CAAC,CAAC,WAAW,SAAS,UAAU,MAAM,MAAM,CAAC;CAEtF,GAAG;EAAC,MAAM;EAAQ,MAAM;EAAU;CAAQ,CAAC;CAE3C,OAAO,cAAcA,mBAAsB,EAAE,SAAS,GAAG,MAAM,QAAQ;AACzE;AAGA,SAAgB,kBAGG;CACjB,MAAM,WAAWC,aAAW;CAC5B,MAAM,SAAS,UAAU;CACzB,OAAO,YACL,OAAO,QAAQ,YAAY;EACzB,MAAM,aAAa,UAAU,QAAQ;GAAE,QAAQ,gBAAgB;GAAQ,GAAG;EAAQ,CAAC;EACnF,OAAO,QAAQ;CACjB,GACA,CAAC,UAAU,MAAM,CACnB;AACF"}
package/dist/index.cjs ADDED
@@ -0,0 +1,162 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_path = require("node:path");
3
+ let node_fs = require("node:fs");
4
+ //#region src/codegen.ts
5
+ const GENERATED_DIR = ".verbaly";
6
+ function generatedDir(root) {
7
+ return (0, node_path.join)(root, GENERATED_DIR);
8
+ }
9
+ function writeIfChanged(file, content) {
10
+ try {
11
+ if ((0, node_fs.readFileSync)(file, "utf8") === content) return false;
12
+ } catch {}
13
+ (0, node_fs.writeFileSync)(file, content);
14
+ return true;
15
+ }
16
+ function writeGeneratedModules(compiler, cfg, catalogs, requestOptions = {}) {
17
+ const dir = generatedDir(cfg.root);
18
+ const localeDir = (0, node_path.join)(dir, "locale");
19
+ (0, node_fs.mkdirSync)(localeDir, { recursive: true });
20
+ let changed = writeIfChanged((0, node_path.join)(dir, ".gitignore"), "*\n");
21
+ const runtime = compiler.generateRuntimeModule(cfg, {
22
+ localeImport: (locale) => `./locale/${locale}.js`,
23
+ extraExports: `export const requestOptions = ${JSON.stringify(requestOptions)};\n`
24
+ });
25
+ changed = writeIfChanged((0, node_path.join)(dir, "index.js"), runtime) || changed;
26
+ const expected = new Set(cfg.locales.map((locale) => `${locale}.js`));
27
+ for (const locale of cfg.locales) changed = writeIfChanged((0, node_path.join)(localeDir, `${locale}.js`), compiler.generateLocaleModule(catalogs[locale] ?? {})) || changed;
28
+ for (const file of (0, node_fs.readdirSync)(localeDir)) if (!expected.has(file)) {
29
+ (0, node_fs.rmSync)((0, node_path.join)(localeDir, file));
30
+ changed = true;
31
+ }
32
+ return changed;
33
+ }
34
+ //#endregion
35
+ //#region src/watch.ts
36
+ const active = /* @__PURE__ */ new Map();
37
+ function startWatcher(compiler, cfg, requestOptions) {
38
+ const existing = active.get(cfg.root);
39
+ if (existing) return existing;
40
+ const catalogDir = (0, node_path.relative)(cfg.root, cfg.dir).replaceAll("\\", "/");
41
+ let timer;
42
+ let running = false;
43
+ let queued = false;
44
+ async function refresh() {
45
+ if (running) {
46
+ queued = true;
47
+ return;
48
+ }
49
+ running = true;
50
+ try {
51
+ const catalogs = compiler.loadCatalogs(cfg);
52
+ const registry = await compiler.extractProject(cfg);
53
+ const { added } = compiler.syncCatalogs(cfg, catalogs, registry);
54
+ for (const locale of Object.keys(added)) compiler.writeCatalog(cfg, locale, catalogs[locale] ?? {});
55
+ compiler.writeDts(cfg, new Map(Object.entries(catalogs[cfg.sourceLocale] ?? {})));
56
+ writeGeneratedModules(compiler, cfg, catalogs, requestOptions);
57
+ } catch (error) {
58
+ console.warn("[verbaly] live extraction failed:", error);
59
+ } finally {
60
+ running = false;
61
+ if (queued) {
62
+ queued = false;
63
+ schedule();
64
+ }
65
+ }
66
+ }
67
+ function schedule() {
68
+ clearTimeout(timer);
69
+ timer = setTimeout(() => void refresh(), 150);
70
+ }
71
+ const watcher = (0, node_fs.watch)(cfg.root, { recursive: true }, (_event, filename) => {
72
+ if (!filename) return;
73
+ const file = filename.replaceAll("\\", "/");
74
+ if (file.startsWith(`.verbaly/`) || file.startsWith(".next/") || file.includes("node_modules/") || file.endsWith(".d.ts")) return;
75
+ if (file.startsWith(`${catalogDir}/`) && file.endsWith(".json") || compiler.SOURCE_FILE_RE.test(file)) schedule();
76
+ });
77
+ watcher.unref?.();
78
+ const dispose = () => {
79
+ clearTimeout(timer);
80
+ watcher.close();
81
+ active.delete(cfg.root);
82
+ };
83
+ active.set(cfg.root, dispose);
84
+ return dispose;
85
+ }
86
+ //#endregion
87
+ //#region src/index.ts
88
+ const DEV_PHASE = "phase-development-server";
89
+ const BUILD_PHASE = "phase-production-build";
90
+ const LOADER = "@verbaly/next/loader";
91
+ const SOURCE_PATH_RE = /\.[cm]?[jt]sx?$/;
92
+ function withVerbaly(nextConfig, options = {}) {
93
+ const { failOnMissing, cookie, fallback, ...verbalyConfig } = options;
94
+ const requestOptions = {
95
+ cookie,
96
+ fallback
97
+ };
98
+ return async (phase, context = {}) => {
99
+ const base = typeof nextConfig === "function" ? await nextConfig(phase, context) : nextConfig ?? {};
100
+ const root = verbalyConfig.root ?? process.cwd();
101
+ if (phase !== DEV_PHASE && phase !== BUILD_PHASE) return composeConfig(base, root);
102
+ const compiler = await import("@verbaly/compiler");
103
+ const cfg = await compiler.loadConfig(root, verbalyConfig);
104
+ const catalogs = compiler.loadCatalogs(cfg);
105
+ const registry = await compiler.extractProject(cfg);
106
+ if (phase === BUILD_PHASE) {
107
+ if (failOnMissing !== false) compiler.runBuildGate(cfg, registry);
108
+ } else {
109
+ const { added } = compiler.syncCatalogs(cfg, catalogs, registry);
110
+ for (const locale of Object.keys(added)) compiler.writeCatalog(cfg, locale, catalogs[locale] ?? {});
111
+ compiler.writeDts(cfg, new Map(Object.entries(catalogs[cfg.sourceLocale] ?? {})));
112
+ startWatcher(compiler, cfg, requestOptions);
113
+ }
114
+ writeGeneratedModules(compiler, cfg, catalogs, requestOptions);
115
+ return composeConfig(base, cfg.root);
116
+ };
117
+ }
118
+ function composeConfig(base, root) {
119
+ const runtimeModule = (0, node_path.join)(generatedDir(root), "index.js");
120
+ const { webpack: userWebpack, turbopack } = base;
121
+ const rules = { ...turbopack?.rules };
122
+ const verbalyRule = {
123
+ condition: { all: [{ not: "foreign" }, { path: SOURCE_PATH_RE }] },
124
+ loaders: [LOADER]
125
+ };
126
+ const existing = rules["*"];
127
+ rules["*"] = existing ? [...Array.isArray(existing) ? existing : [existing], verbalyRule] : verbalyRule;
128
+ return {
129
+ ...base,
130
+ turbopack: {
131
+ ...turbopack,
132
+ resolveAlias: {
133
+ ...turbopack?.resolveAlias,
134
+ "virtual:verbaly": `./${GENERATED_DIR}/index.js`
135
+ },
136
+ rules
137
+ },
138
+ webpack(config, context) {
139
+ const { webpack: webpackInstance } = context ?? {};
140
+ if (webpackInstance?.NormalModuleReplacementPlugin) {
141
+ config.plugins ??= [];
142
+ config.plugins.push(new webpackInstance.NormalModuleReplacementPlugin(/^virtual:verbaly$/, runtimeModule));
143
+ }
144
+ config.resolve ??= {};
145
+ config.resolve.alias ??= {};
146
+ config.resolve.alias["virtual:verbaly"] = runtimeModule;
147
+ config.module ??= {};
148
+ config.module.rules ??= [];
149
+ config.module.rules.push({
150
+ test: /\.[cm]?[jt]sx?$/,
151
+ exclude: /node_modules/,
152
+ enforce: "pre",
153
+ use: [{ loader: LOADER }]
154
+ });
155
+ return userWebpack ? userWebpack(config, context) : config;
156
+ }
157
+ };
158
+ }
159
+ //#endregion
160
+ exports.withVerbaly = withVerbaly;
161
+
162
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/codegen.ts","../src/watch.ts","../src/index.ts"],"sourcesContent":["import type { Catalogs, ResolvedConfig } from '@verbaly/compiler';\nimport { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\n// the compiler is ESM-only; the dual (CJS-consumable) root entry loads it via\n// dynamic import once and threads the module through\nexport type Compiler = typeof import('@verbaly/compiler');\n\nexport interface RequestOptions {\n cookie?: string | false;\n fallback?: string;\n}\n\nexport const GENERATED_DIR = '.verbaly';\n\nexport function generatedDir(root: string): string {\n return join(root, GENERATED_DIR);\n}\n\n// content compare — identical rewrites must not retrigger the bundler\nfunction writeIfChanged(file: string, content: string): boolean {\n try {\n if (readFileSync(file, 'utf8') === content) return false;\n } catch {\n // new file\n }\n writeFileSync(file, content);\n return true;\n}\n\n// real-file replacement for virtual:verbaly — Turbopack has no virtual modules\nexport function writeGeneratedModules(\n compiler: Compiler,\n cfg: ResolvedConfig,\n catalogs: Catalogs,\n requestOptions: RequestOptions = {},\n): boolean {\n const dir = generatedDir(cfg.root);\n const localeDir = join(dir, 'locale');\n mkdirSync(localeDir, { recursive: true });\n\n let changed = writeIfChanged(join(dir, '.gitignore'), '*\\n');\n\n const runtime = compiler.generateRuntimeModule(cfg, {\n localeImport: (locale) => `./locale/${locale}.js`,\n extraExports: `export const requestOptions = ${JSON.stringify(requestOptions)};\\n`,\n });\n changed = writeIfChanged(join(dir, 'index.js'), runtime) || changed;\n\n const expected = new Set(cfg.locales.map((locale) => `${locale}.js`));\n for (const locale of cfg.locales) {\n changed =\n writeIfChanged(\n join(localeDir, `${locale}.js`),\n compiler.generateLocaleModule(catalogs[locale] ?? {}),\n ) || changed;\n }\n for (const file of readdirSync(localeDir)) {\n if (!expected.has(file)) {\n rmSync(join(localeDir, file));\n changed = true;\n }\n }\n return changed;\n}\n","import type { ResolvedConfig } from '@verbaly/compiler';\nimport { watch } from 'node:fs';\nimport { relative } from 'node:path';\nimport { GENERATED_DIR, writeGeneratedModules, type Compiler, type RequestOptions } from './codegen';\n\n// one watcher per project root — next.config can be evaluated more than once\nconst active = new Map<string, () => void>();\n\nexport function startWatcher(\n compiler: Compiler,\n cfg: ResolvedConfig,\n requestOptions: RequestOptions,\n): () => void {\n const existing = active.get(cfg.root);\n if (existing) return existing;\n\n const catalogDir = relative(cfg.root, cfg.dir).replaceAll('\\\\', '/');\n let timer: ReturnType<typeof setTimeout> | undefined;\n let running = false;\n let queued = false;\n\n async function refresh(): Promise<void> {\n if (running) {\n queued = true;\n return;\n }\n running = true;\n try {\n const catalogs = compiler.loadCatalogs(cfg);\n const registry = await compiler.extractProject(cfg);\n const { added } = compiler.syncCatalogs(cfg, catalogs, registry);\n for (const locale of Object.keys(added)) {\n compiler.writeCatalog(cfg, locale, catalogs[locale] ?? {});\n }\n compiler.writeDts(cfg, new Map(Object.entries(catalogs[cfg.sourceLocale] ?? {})));\n writeGeneratedModules(compiler, cfg, catalogs, requestOptions);\n } catch (error) {\n console.warn('[verbaly] live extraction failed:', error);\n } finally {\n running = false;\n if (queued) {\n queued = false;\n schedule();\n }\n }\n }\n\n function schedule(): void {\n clearTimeout(timer);\n timer = setTimeout(() => void refresh(), 150);\n }\n\n const watcher = watch(cfg.root, { recursive: true }, (_event, filename) => {\n if (!filename) return;\n const file = filename.replaceAll('\\\\', '/');\n if (\n file.startsWith(`${GENERATED_DIR}/`) ||\n file.startsWith('.next/') ||\n file.includes('node_modules/') ||\n file.endsWith('.d.ts')\n ) {\n return;\n }\n const isCatalog = file.startsWith(`${catalogDir}/`) && file.endsWith('.json');\n if (isCatalog || compiler.SOURCE_FILE_RE.test(file)) schedule();\n });\n watcher.unref?.();\n\n const dispose = (): void => {\n clearTimeout(timer);\n watcher.close();\n active.delete(cfg.root);\n };\n active.set(cfg.root, dispose);\n return dispose;\n}\n\n// test hook\nexport function stopWatcher(root: string): void {\n active.get(root)?.();\n}\n","import type { VerbalyConfig } from '@verbaly/compiler';\nimport { join } from 'node:path';\nimport {\n GENERATED_DIR,\n generatedDir,\n writeGeneratedModules,\n type Compiler,\n type RequestOptions,\n} from './codegen';\nimport { startWatcher } from './watch';\n\nexport type { VerbalyConfig } from '@verbaly/compiler';\nexport type { RequestOptions } from './codegen';\n\nexport interface NextVerbalyOptions extends VerbalyConfig {\n // opt out of the build gate (parity with @verbaly/vite and @verbaly/unplugin)\n failOnMissing?: boolean;\n // negotiation cookie (false = Accept-Language only); defaults to core's LOCALE_STORAGE_KEY\n cookie?: string | false;\n // negotiation fallback; defaults to the source locale\n fallback?: string;\n}\n\n// structural NextConfig subset — compat asserted against next's real type in tests.\n// No index signatures: Next's real interfaces have none and a target index\n// signature would reject them.\nexport interface WebpackConfigLike {\n resolve?: { alias?: Record<string, unknown>; [key: string]: unknown };\n module?: { rules?: unknown[]; [key: string]: unknown };\n plugins?: unknown[];\n [key: string]: unknown;\n}\n\ninterface WebpackContextLike {\n webpack?: {\n NormalModuleReplacementPlugin: new (test: RegExp, resource: string) => unknown;\n };\n}\n\n// context param is `never` so Next's real webpack fn (specific context type) stays assignable\nexport type WebpackFn = (config: WebpackConfigLike, context: never) => unknown;\n\nexport interface TurbopackLike {\n resolveAlias?: Record<string, unknown>;\n rules?: Record<string, unknown>;\n}\n\nexport interface NextConfigLike {\n webpack?: WebpackFn | null;\n turbopack?: TurbopackLike;\n}\n\n// constraint is `object` (not NextConfigLike) — an all-optional target would\n// reject configs sharing no keys with it (TS weak-type rule)\nexport type NextConfigInput<C extends object> =\n | C\n | ((phase: string, context: { defaultConfig?: unknown }) => C | Promise<C>);\n\n// next/constants values — literal to keep this module import-free of next\nconst DEV_PHASE = 'phase-development-server';\nconst BUILD_PHASE = 'phase-production-build';\n\nconst LOADER = '@verbaly/next/loader';\n// compiler's SOURCE_FILE_RE (inlined: the compiler is only dynamically imported here).\n// Matching happens via `condition.path` — a bare extension glob also matches Next's\n// internal App Router entry and Turbopack panics reading it as a file.\nconst SOURCE_PATH_RE = /\\.[cm]?[jt]sx?$/;\n\nexport function withVerbaly<C extends object>(\n nextConfig?: NextConfigInput<C>,\n options: NextVerbalyOptions = {},\n): (phase: string, context?: { defaultConfig?: unknown }) => Promise<C> {\n const { failOnMissing, cookie, fallback, ...verbalyConfig } = options;\n const requestOptions: RequestOptions = { cookie, fallback };\n\n return async (phase, context = {}) => {\n const base: C =\n typeof nextConfig === 'function' ? await nextConfig(phase, context) : (nextConfig ?? ({} as C));\n const root = verbalyConfig.root ?? process.cwd();\n\n // production server / export: everything is bundled — no FS work, config only\n if (phase !== DEV_PHASE && phase !== BUILD_PHASE) {\n return composeConfig(base, root);\n }\n\n // dynamic: the compiler is ESM-only and this entry is also consumed as CJS\n const compiler: Compiler = await import('@verbaly/compiler');\n\n const cfg = await compiler.loadConfig(root, verbalyConfig);\n const catalogs = compiler.loadCatalogs(cfg);\n const registry = await compiler.extractProject(cfg);\n\n if (phase === BUILD_PHASE) {\n if (failOnMissing !== false) compiler.runBuildGate(cfg, registry);\n } else {\n const { added } = compiler.syncCatalogs(cfg, catalogs, registry);\n for (const locale of Object.keys(added)) {\n compiler.writeCatalog(cfg, locale, catalogs[locale] ?? {});\n }\n compiler.writeDts(cfg, new Map(Object.entries(catalogs[cfg.sourceLocale] ?? {})));\n startWatcher(compiler, cfg, requestOptions);\n }\n\n writeGeneratedModules(compiler, cfg, catalogs, requestOptions);\n return composeConfig(base, cfg.root);\n };\n}\n\nfunction composeConfig<C extends object>(base: C, root: string): C {\n const runtimeModule = join(generatedDir(root), 'index.js');\n const { webpack: userWebpack, turbopack } = base as NextConfigLike;\n\n const rules: Record<string, unknown> = { ...turbopack?.rules };\n const verbalyRule = {\n condition: { all: [{ not: 'foreign' }, { path: SOURCE_PATH_RE }] },\n loaders: [LOADER],\n };\n const existing = rules['*'];\n rules['*'] = existing\n ? [...(Array.isArray(existing) ? existing : [existing]), verbalyRule]\n : verbalyRule;\n\n return {\n ...base,\n turbopack: {\n ...turbopack,\n resolveAlias: {\n ...turbopack?.resolveAlias,\n 'virtual:verbaly': `./${GENERATED_DIR}/index.js`,\n },\n rules,\n },\n webpack(config: WebpackConfigLike, context: unknown) {\n // webpack 5 treats 'virtual:' as a URI scheme — resolve.alias never fires,\n // module replacement does; the alias stays as a fallback for odd setups\n const { webpack: webpackInstance } = (context ?? {}) as WebpackContextLike;\n if (webpackInstance?.NormalModuleReplacementPlugin) {\n config.plugins ??= [];\n config.plugins.push(\n new webpackInstance.NormalModuleReplacementPlugin(/^virtual:verbaly$/, runtimeModule),\n );\n }\n config.resolve ??= {};\n config.resolve.alias ??= {};\n (config.resolve.alias as Record<string, unknown>)['virtual:verbaly'] = runtimeModule;\n config.module ??= {};\n config.module.rules ??= [];\n config.module.rules.push({\n test: /\\.[cm]?[jt]sx?$/,\n exclude: /node_modules/,\n enforce: 'pre',\n use: [{ loader: LOADER }],\n });\n return userWebpack ? userWebpack(config, context as never) : config;\n },\n } as C;\n}\n"],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import { VerbalyConfig, VerbalyConfig as VerbalyConfig$1 } from "@verbaly/compiler";
2
+ //#region src/codegen.d.ts
3
+ interface RequestOptions {
4
+ cookie?: string | false;
5
+ fallback?: string;
6
+ }
7
+ //#endregion
8
+ //#region src/index.d.ts
9
+ interface NextVerbalyOptions extends VerbalyConfig$1 {
10
+ failOnMissing?: boolean;
11
+ cookie?: string | false;
12
+ fallback?: string;
13
+ }
14
+ interface WebpackConfigLike {
15
+ resolve?: {
16
+ alias?: Record<string, unknown>;
17
+ [key: string]: unknown;
18
+ };
19
+ module?: {
20
+ rules?: unknown[];
21
+ [key: string]: unknown;
22
+ };
23
+ plugins?: unknown[];
24
+ [key: string]: unknown;
25
+ }
26
+ type WebpackFn = (config: WebpackConfigLike, context: never) => unknown;
27
+ interface TurbopackLike {
28
+ resolveAlias?: Record<string, unknown>;
29
+ rules?: Record<string, unknown>;
30
+ }
31
+ interface NextConfigLike {
32
+ webpack?: WebpackFn | null;
33
+ turbopack?: TurbopackLike;
34
+ }
35
+ type NextConfigInput<C extends object> = C | ((phase: string, context: {
36
+ defaultConfig?: unknown;
37
+ }) => C | Promise<C>);
38
+ declare function withVerbaly<C extends object>(nextConfig?: NextConfigInput<C>, options?: NextVerbalyOptions): (phase: string, context?: {
39
+ defaultConfig?: unknown;
40
+ }) => Promise<C>;
41
+ //#endregion
42
+ export { NextConfigInput, NextConfigLike, NextVerbalyOptions, type RequestOptions, TurbopackLike, type VerbalyConfig, WebpackConfigLike, WebpackFn, withVerbaly };
43
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1,43 @@
1
+ import { VerbalyConfig, VerbalyConfig as VerbalyConfig$1 } from "@verbaly/compiler";
2
+ //#region src/codegen.d.ts
3
+ interface RequestOptions {
4
+ cookie?: string | false;
5
+ fallback?: string;
6
+ }
7
+ //#endregion
8
+ //#region src/index.d.ts
9
+ interface NextVerbalyOptions extends VerbalyConfig$1 {
10
+ failOnMissing?: boolean;
11
+ cookie?: string | false;
12
+ fallback?: string;
13
+ }
14
+ interface WebpackConfigLike {
15
+ resolve?: {
16
+ alias?: Record<string, unknown>;
17
+ [key: string]: unknown;
18
+ };
19
+ module?: {
20
+ rules?: unknown[];
21
+ [key: string]: unknown;
22
+ };
23
+ plugins?: unknown[];
24
+ [key: string]: unknown;
25
+ }
26
+ type WebpackFn = (config: WebpackConfigLike, context: never) => unknown;
27
+ interface TurbopackLike {
28
+ resolveAlias?: Record<string, unknown>;
29
+ rules?: Record<string, unknown>;
30
+ }
31
+ interface NextConfigLike {
32
+ webpack?: WebpackFn | null;
33
+ turbopack?: TurbopackLike;
34
+ }
35
+ type NextConfigInput<C extends object> = C | ((phase: string, context: {
36
+ defaultConfig?: unknown;
37
+ }) => C | Promise<C>);
38
+ declare function withVerbaly<C extends object>(nextConfig?: NextConfigInput<C>, options?: NextVerbalyOptions): (phase: string, context?: {
39
+ defaultConfig?: unknown;
40
+ }) => Promise<C>;
41
+ //#endregion
42
+ export { NextConfigInput, NextConfigLike, NextVerbalyOptions, type RequestOptions, TurbopackLike, type VerbalyConfig, WebpackConfigLike, WebpackFn, withVerbaly };
43
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,161 @@
1
+ import { join, relative } from "node:path";
2
+ import { mkdirSync, readFileSync, readdirSync, rmSync, watch, writeFileSync } from "node:fs";
3
+ //#region src/codegen.ts
4
+ const GENERATED_DIR = ".verbaly";
5
+ function generatedDir(root) {
6
+ return join(root, GENERATED_DIR);
7
+ }
8
+ function writeIfChanged(file, content) {
9
+ try {
10
+ if (readFileSync(file, "utf8") === content) return false;
11
+ } catch {}
12
+ writeFileSync(file, content);
13
+ return true;
14
+ }
15
+ function writeGeneratedModules(compiler, cfg, catalogs, requestOptions = {}) {
16
+ const dir = generatedDir(cfg.root);
17
+ const localeDir = join(dir, "locale");
18
+ mkdirSync(localeDir, { recursive: true });
19
+ let changed = writeIfChanged(join(dir, ".gitignore"), "*\n");
20
+ const runtime = compiler.generateRuntimeModule(cfg, {
21
+ localeImport: (locale) => `./locale/${locale}.js`,
22
+ extraExports: `export const requestOptions = ${JSON.stringify(requestOptions)};\n`
23
+ });
24
+ changed = writeIfChanged(join(dir, "index.js"), runtime) || changed;
25
+ const expected = new Set(cfg.locales.map((locale) => `${locale}.js`));
26
+ for (const locale of cfg.locales) changed = writeIfChanged(join(localeDir, `${locale}.js`), compiler.generateLocaleModule(catalogs[locale] ?? {})) || changed;
27
+ for (const file of readdirSync(localeDir)) if (!expected.has(file)) {
28
+ rmSync(join(localeDir, file));
29
+ changed = true;
30
+ }
31
+ return changed;
32
+ }
33
+ //#endregion
34
+ //#region src/watch.ts
35
+ const active = /* @__PURE__ */ new Map();
36
+ function startWatcher(compiler, cfg, requestOptions) {
37
+ const existing = active.get(cfg.root);
38
+ if (existing) return existing;
39
+ const catalogDir = relative(cfg.root, cfg.dir).replaceAll("\\", "/");
40
+ let timer;
41
+ let running = false;
42
+ let queued = false;
43
+ async function refresh() {
44
+ if (running) {
45
+ queued = true;
46
+ return;
47
+ }
48
+ running = true;
49
+ try {
50
+ const catalogs = compiler.loadCatalogs(cfg);
51
+ const registry = await compiler.extractProject(cfg);
52
+ const { added } = compiler.syncCatalogs(cfg, catalogs, registry);
53
+ for (const locale of Object.keys(added)) compiler.writeCatalog(cfg, locale, catalogs[locale] ?? {});
54
+ compiler.writeDts(cfg, new Map(Object.entries(catalogs[cfg.sourceLocale] ?? {})));
55
+ writeGeneratedModules(compiler, cfg, catalogs, requestOptions);
56
+ } catch (error) {
57
+ console.warn("[verbaly] live extraction failed:", error);
58
+ } finally {
59
+ running = false;
60
+ if (queued) {
61
+ queued = false;
62
+ schedule();
63
+ }
64
+ }
65
+ }
66
+ function schedule() {
67
+ clearTimeout(timer);
68
+ timer = setTimeout(() => void refresh(), 150);
69
+ }
70
+ const watcher = watch(cfg.root, { recursive: true }, (_event, filename) => {
71
+ if (!filename) return;
72
+ const file = filename.replaceAll("\\", "/");
73
+ if (file.startsWith(`.verbaly/`) || file.startsWith(".next/") || file.includes("node_modules/") || file.endsWith(".d.ts")) return;
74
+ if (file.startsWith(`${catalogDir}/`) && file.endsWith(".json") || compiler.SOURCE_FILE_RE.test(file)) schedule();
75
+ });
76
+ watcher.unref?.();
77
+ const dispose = () => {
78
+ clearTimeout(timer);
79
+ watcher.close();
80
+ active.delete(cfg.root);
81
+ };
82
+ active.set(cfg.root, dispose);
83
+ return dispose;
84
+ }
85
+ //#endregion
86
+ //#region src/index.ts
87
+ const DEV_PHASE = "phase-development-server";
88
+ const BUILD_PHASE = "phase-production-build";
89
+ const LOADER = "@verbaly/next/loader";
90
+ const SOURCE_PATH_RE = /\.[cm]?[jt]sx?$/;
91
+ function withVerbaly(nextConfig, options = {}) {
92
+ const { failOnMissing, cookie, fallback, ...verbalyConfig } = options;
93
+ const requestOptions = {
94
+ cookie,
95
+ fallback
96
+ };
97
+ return async (phase, context = {}) => {
98
+ const base = typeof nextConfig === "function" ? await nextConfig(phase, context) : nextConfig ?? {};
99
+ const root = verbalyConfig.root ?? process.cwd();
100
+ if (phase !== DEV_PHASE && phase !== BUILD_PHASE) return composeConfig(base, root);
101
+ const compiler = await import("@verbaly/compiler");
102
+ const cfg = await compiler.loadConfig(root, verbalyConfig);
103
+ const catalogs = compiler.loadCatalogs(cfg);
104
+ const registry = await compiler.extractProject(cfg);
105
+ if (phase === BUILD_PHASE) {
106
+ if (failOnMissing !== false) compiler.runBuildGate(cfg, registry);
107
+ } else {
108
+ const { added } = compiler.syncCatalogs(cfg, catalogs, registry);
109
+ for (const locale of Object.keys(added)) compiler.writeCatalog(cfg, locale, catalogs[locale] ?? {});
110
+ compiler.writeDts(cfg, new Map(Object.entries(catalogs[cfg.sourceLocale] ?? {})));
111
+ startWatcher(compiler, cfg, requestOptions);
112
+ }
113
+ writeGeneratedModules(compiler, cfg, catalogs, requestOptions);
114
+ return composeConfig(base, cfg.root);
115
+ };
116
+ }
117
+ function composeConfig(base, root) {
118
+ const runtimeModule = join(generatedDir(root), "index.js");
119
+ const { webpack: userWebpack, turbopack } = base;
120
+ const rules = { ...turbopack?.rules };
121
+ const verbalyRule = {
122
+ condition: { all: [{ not: "foreign" }, { path: SOURCE_PATH_RE }] },
123
+ loaders: [LOADER]
124
+ };
125
+ const existing = rules["*"];
126
+ rules["*"] = existing ? [...Array.isArray(existing) ? existing : [existing], verbalyRule] : verbalyRule;
127
+ return {
128
+ ...base,
129
+ turbopack: {
130
+ ...turbopack,
131
+ resolveAlias: {
132
+ ...turbopack?.resolveAlias,
133
+ "virtual:verbaly": `./${GENERATED_DIR}/index.js`
134
+ },
135
+ rules
136
+ },
137
+ webpack(config, context) {
138
+ const { webpack: webpackInstance } = context ?? {};
139
+ if (webpackInstance?.NormalModuleReplacementPlugin) {
140
+ config.plugins ??= [];
141
+ config.plugins.push(new webpackInstance.NormalModuleReplacementPlugin(/^virtual:verbaly$/, runtimeModule));
142
+ }
143
+ config.resolve ??= {};
144
+ config.resolve.alias ??= {};
145
+ config.resolve.alias["virtual:verbaly"] = runtimeModule;
146
+ config.module ??= {};
147
+ config.module.rules ??= [];
148
+ config.module.rules.push({
149
+ test: /\.[cm]?[jt]sx?$/,
150
+ exclude: /node_modules/,
151
+ enforce: "pre",
152
+ use: [{ loader: LOADER }]
153
+ });
154
+ return userWebpack ? userWebpack(config, context) : config;
155
+ }
156
+ };
157
+ }
158
+ //#endregion
159
+ export { withVerbaly };
160
+
161
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/codegen.ts","../src/watch.ts","../src/index.ts"],"sourcesContent":["import type { Catalogs, ResolvedConfig } from '@verbaly/compiler';\nimport { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\n// the compiler is ESM-only; the dual (CJS-consumable) root entry loads it via\n// dynamic import once and threads the module through\nexport type Compiler = typeof import('@verbaly/compiler');\n\nexport interface RequestOptions {\n cookie?: string | false;\n fallback?: string;\n}\n\nexport const GENERATED_DIR = '.verbaly';\n\nexport function generatedDir(root: string): string {\n return join(root, GENERATED_DIR);\n}\n\n// content compare — identical rewrites must not retrigger the bundler\nfunction writeIfChanged(file: string, content: string): boolean {\n try {\n if (readFileSync(file, 'utf8') === content) return false;\n } catch {\n // new file\n }\n writeFileSync(file, content);\n return true;\n}\n\n// real-file replacement for virtual:verbaly — Turbopack has no virtual modules\nexport function writeGeneratedModules(\n compiler: Compiler,\n cfg: ResolvedConfig,\n catalogs: Catalogs,\n requestOptions: RequestOptions = {},\n): boolean {\n const dir = generatedDir(cfg.root);\n const localeDir = join(dir, 'locale');\n mkdirSync(localeDir, { recursive: true });\n\n let changed = writeIfChanged(join(dir, '.gitignore'), '*\\n');\n\n const runtime = compiler.generateRuntimeModule(cfg, {\n localeImport: (locale) => `./locale/${locale}.js`,\n extraExports: `export const requestOptions = ${JSON.stringify(requestOptions)};\\n`,\n });\n changed = writeIfChanged(join(dir, 'index.js'), runtime) || changed;\n\n const expected = new Set(cfg.locales.map((locale) => `${locale}.js`));\n for (const locale of cfg.locales) {\n changed =\n writeIfChanged(\n join(localeDir, `${locale}.js`),\n compiler.generateLocaleModule(catalogs[locale] ?? {}),\n ) || changed;\n }\n for (const file of readdirSync(localeDir)) {\n if (!expected.has(file)) {\n rmSync(join(localeDir, file));\n changed = true;\n }\n }\n return changed;\n}\n","import type { ResolvedConfig } from '@verbaly/compiler';\nimport { watch } from 'node:fs';\nimport { relative } from 'node:path';\nimport { GENERATED_DIR, writeGeneratedModules, type Compiler, type RequestOptions } from './codegen';\n\n// one watcher per project root — next.config can be evaluated more than once\nconst active = new Map<string, () => void>();\n\nexport function startWatcher(\n compiler: Compiler,\n cfg: ResolvedConfig,\n requestOptions: RequestOptions,\n): () => void {\n const existing = active.get(cfg.root);\n if (existing) return existing;\n\n const catalogDir = relative(cfg.root, cfg.dir).replaceAll('\\\\', '/');\n let timer: ReturnType<typeof setTimeout> | undefined;\n let running = false;\n let queued = false;\n\n async function refresh(): Promise<void> {\n if (running) {\n queued = true;\n return;\n }\n running = true;\n try {\n const catalogs = compiler.loadCatalogs(cfg);\n const registry = await compiler.extractProject(cfg);\n const { added } = compiler.syncCatalogs(cfg, catalogs, registry);\n for (const locale of Object.keys(added)) {\n compiler.writeCatalog(cfg, locale, catalogs[locale] ?? {});\n }\n compiler.writeDts(cfg, new Map(Object.entries(catalogs[cfg.sourceLocale] ?? {})));\n writeGeneratedModules(compiler, cfg, catalogs, requestOptions);\n } catch (error) {\n console.warn('[verbaly] live extraction failed:', error);\n } finally {\n running = false;\n if (queued) {\n queued = false;\n schedule();\n }\n }\n }\n\n function schedule(): void {\n clearTimeout(timer);\n timer = setTimeout(() => void refresh(), 150);\n }\n\n const watcher = watch(cfg.root, { recursive: true }, (_event, filename) => {\n if (!filename) return;\n const file = filename.replaceAll('\\\\', '/');\n if (\n file.startsWith(`${GENERATED_DIR}/`) ||\n file.startsWith('.next/') ||\n file.includes('node_modules/') ||\n file.endsWith('.d.ts')\n ) {\n return;\n }\n const isCatalog = file.startsWith(`${catalogDir}/`) && file.endsWith('.json');\n if (isCatalog || compiler.SOURCE_FILE_RE.test(file)) schedule();\n });\n watcher.unref?.();\n\n const dispose = (): void => {\n clearTimeout(timer);\n watcher.close();\n active.delete(cfg.root);\n };\n active.set(cfg.root, dispose);\n return dispose;\n}\n\n// test hook\nexport function stopWatcher(root: string): void {\n active.get(root)?.();\n}\n","import type { VerbalyConfig } from '@verbaly/compiler';\nimport { join } from 'node:path';\nimport {\n GENERATED_DIR,\n generatedDir,\n writeGeneratedModules,\n type Compiler,\n type RequestOptions,\n} from './codegen';\nimport { startWatcher } from './watch';\n\nexport type { VerbalyConfig } from '@verbaly/compiler';\nexport type { RequestOptions } from './codegen';\n\nexport interface NextVerbalyOptions extends VerbalyConfig {\n // opt out of the build gate (parity with @verbaly/vite and @verbaly/unplugin)\n failOnMissing?: boolean;\n // negotiation cookie (false = Accept-Language only); defaults to core's LOCALE_STORAGE_KEY\n cookie?: string | false;\n // negotiation fallback; defaults to the source locale\n fallback?: string;\n}\n\n// structural NextConfig subset — compat asserted against next's real type in tests.\n// No index signatures: Next's real interfaces have none and a target index\n// signature would reject them.\nexport interface WebpackConfigLike {\n resolve?: { alias?: Record<string, unknown>; [key: string]: unknown };\n module?: { rules?: unknown[]; [key: string]: unknown };\n plugins?: unknown[];\n [key: string]: unknown;\n}\n\ninterface WebpackContextLike {\n webpack?: {\n NormalModuleReplacementPlugin: new (test: RegExp, resource: string) => unknown;\n };\n}\n\n// context param is `never` so Next's real webpack fn (specific context type) stays assignable\nexport type WebpackFn = (config: WebpackConfigLike, context: never) => unknown;\n\nexport interface TurbopackLike {\n resolveAlias?: Record<string, unknown>;\n rules?: Record<string, unknown>;\n}\n\nexport interface NextConfigLike {\n webpack?: WebpackFn | null;\n turbopack?: TurbopackLike;\n}\n\n// constraint is `object` (not NextConfigLike) — an all-optional target would\n// reject configs sharing no keys with it (TS weak-type rule)\nexport type NextConfigInput<C extends object> =\n | C\n | ((phase: string, context: { defaultConfig?: unknown }) => C | Promise<C>);\n\n// next/constants values — literal to keep this module import-free of next\nconst DEV_PHASE = 'phase-development-server';\nconst BUILD_PHASE = 'phase-production-build';\n\nconst LOADER = '@verbaly/next/loader';\n// compiler's SOURCE_FILE_RE (inlined: the compiler is only dynamically imported here).\n// Matching happens via `condition.path` — a bare extension glob also matches Next's\n// internal App Router entry and Turbopack panics reading it as a file.\nconst SOURCE_PATH_RE = /\\.[cm]?[jt]sx?$/;\n\nexport function withVerbaly<C extends object>(\n nextConfig?: NextConfigInput<C>,\n options: NextVerbalyOptions = {},\n): (phase: string, context?: { defaultConfig?: unknown }) => Promise<C> {\n const { failOnMissing, cookie, fallback, ...verbalyConfig } = options;\n const requestOptions: RequestOptions = { cookie, fallback };\n\n return async (phase, context = {}) => {\n const base: C =\n typeof nextConfig === 'function' ? await nextConfig(phase, context) : (nextConfig ?? ({} as C));\n const root = verbalyConfig.root ?? process.cwd();\n\n // production server / export: everything is bundled — no FS work, config only\n if (phase !== DEV_PHASE && phase !== BUILD_PHASE) {\n return composeConfig(base, root);\n }\n\n // dynamic: the compiler is ESM-only and this entry is also consumed as CJS\n const compiler: Compiler = await import('@verbaly/compiler');\n\n const cfg = await compiler.loadConfig(root, verbalyConfig);\n const catalogs = compiler.loadCatalogs(cfg);\n const registry = await compiler.extractProject(cfg);\n\n if (phase === BUILD_PHASE) {\n if (failOnMissing !== false) compiler.runBuildGate(cfg, registry);\n } else {\n const { added } = compiler.syncCatalogs(cfg, catalogs, registry);\n for (const locale of Object.keys(added)) {\n compiler.writeCatalog(cfg, locale, catalogs[locale] ?? {});\n }\n compiler.writeDts(cfg, new Map(Object.entries(catalogs[cfg.sourceLocale] ?? {})));\n startWatcher(compiler, cfg, requestOptions);\n }\n\n writeGeneratedModules(compiler, cfg, catalogs, requestOptions);\n return composeConfig(base, cfg.root);\n };\n}\n\nfunction composeConfig<C extends object>(base: C, root: string): C {\n const runtimeModule = join(generatedDir(root), 'index.js');\n const { webpack: userWebpack, turbopack } = base as NextConfigLike;\n\n const rules: Record<string, unknown> = { ...turbopack?.rules };\n const verbalyRule = {\n condition: { all: [{ not: 'foreign' }, { path: SOURCE_PATH_RE }] },\n loaders: [LOADER],\n };\n const existing = rules['*'];\n rules['*'] = existing\n ? [...(Array.isArray(existing) ? existing : [existing]), verbalyRule]\n : verbalyRule;\n\n return {\n ...base,\n turbopack: {\n ...turbopack,\n resolveAlias: {\n ...turbopack?.resolveAlias,\n 'virtual:verbaly': `./${GENERATED_DIR}/index.js`,\n },\n rules,\n },\n webpack(config: WebpackConfigLike, context: unknown) {\n // webpack 5 treats 'virtual:' as a URI scheme — resolve.alias never fires,\n // module replacement does; the alias stays as a fallback for odd setups\n const { webpack: webpackInstance } = (context ?? {}) as WebpackContextLike;\n if (webpackInstance?.NormalModuleReplacementPlugin) {\n config.plugins ??= [];\n config.plugins.push(\n new webpackInstance.NormalModuleReplacementPlugin(/^virtual:verbaly$/, runtimeModule),\n );\n }\n config.resolve ??= {};\n config.resolve.alias ??= {};\n (config.resolve.alias as Record<string, unknown>)['virtual:verbaly'] = runtimeModule;\n config.module ??= {};\n config.module.rules ??= [];\n config.module.rules.push({\n test: /\\.[cm]?[jt]sx?$/,\n exclude: /node_modules/,\n enforce: 'pre',\n use: [{ loader: LOADER }],\n });\n return userWebpack ? userWebpack(config, context as never) : config;\n },\n } as C;\n}\n"],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ //#region src/loader.ts
2
+ let compiler;
3
+ function verbalyLoader(source) {
4
+ const callback = this.async();
5
+ const file = this.resourcePath;
6
+ compiler ??= import("@verbaly/compiler");
7
+ compiler.then(({ isTransformTarget, transformCode }) => {
8
+ if (!isTransformTarget(file)) {
9
+ callback(null, source);
10
+ return;
11
+ }
12
+ const result = transformCode(source, file);
13
+ if (result) {
14
+ result.map.sources = [file];
15
+ callback(null, result.code, result.map);
16
+ } else callback(null, source);
17
+ }).catch((error) => callback(error));
18
+ }
19
+ //#endregion
20
+ module.exports = verbalyLoader;
21
+
22
+ //# sourceMappingURL=loader.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.cjs","names":[],"sources":["../src/loader.ts"],"sourcesContent":["// webpack/turbopack loader: rewrites t`…` / <Trans> to compiled keys.\n// Async because @verbaly/compiler is ESM-only — loaded via dynamic import.\n\nexport interface LoaderContext {\n resourcePath: string;\n async(): (error: unknown, code?: string, map?: unknown) => void;\n}\n\nlet compiler: Promise<typeof import('@verbaly/compiler')> | undefined;\n\nexport default function verbalyLoader(this: LoaderContext, source: string): void {\n const callback = this.async();\n const file = this.resourcePath;\n compiler ??= import('@verbaly/compiler');\n compiler\n .then(({ isTransformTarget, transformCode }) => {\n if (!isTransformTarget(file)) {\n callback(null, source);\n return;\n }\n const result = transformCode(source, file);\n if (result) {\n // MagicString maps carry an empty source — Turbopack resolves '' to the\n // module's directory and panics trying to read it as a file\n result.map.sources = [file];\n callback(null, result.code, result.map);\n } else {\n callback(null, source);\n }\n })\n .catch((error: unknown) => callback(error));\n}\n"],"mappings":";AAQA,IAAI;AAEJ,SAAwB,cAAmC,QAAsB;CAC/E,MAAM,WAAW,KAAK,MAAM;CAC5B,MAAM,OAAO,KAAK;CAClB,aAAa,OAAO;CACpB,SACG,MAAM,EAAE,mBAAmB,oBAAoB;EAC9C,IAAI,CAAC,kBAAkB,IAAI,GAAG;GAC5B,SAAS,MAAM,MAAM;GACrB;EACF;EACA,MAAM,SAAS,cAAc,QAAQ,IAAI;EACzC,IAAI,QAAQ;GAGV,OAAO,IAAI,UAAU,CAAC,IAAI;GAC1B,SAAS,MAAM,OAAO,MAAM,OAAO,GAAG;EACxC,OACE,SAAS,MAAM,MAAM;CAEzB,CAAC,CAAC,CACD,OAAO,UAAmB,SAAS,KAAK,CAAC;AAC9C"}
@@ -0,0 +1,13 @@
1
+ import { TFunction, Verbaly } from "verbaly";
2
+ //#region src/server.d.ts
3
+ declare function getVerbaly(): Promise<Verbaly>;
4
+ declare function getT(): Promise<TFunction>;
5
+ declare function getRequestLocale(): Promise<string>;
6
+ interface VerbalyProviderProps {
7
+ locale: string;
8
+ messages?: Record<string, string>;
9
+ }
10
+ declare function getVerbalyProps(): Promise<VerbalyProviderProps>;
11
+ //#endregion
12
+ export { VerbalyProviderProps, getRequestLocale, getT, getVerbaly, getVerbalyProps };
13
+ //# sourceMappingURL=server.d.ts.map
package/dist/server.js ADDED
@@ -0,0 +1,40 @@
1
+ import { cookies, headers } from "next/headers.js";
2
+ import * as React from "react";
3
+ import { LOCALE_STORAGE_KEY, resolveRequestLocale } from "verbaly";
4
+ import { createRequestInstance, loadMessages, locales, requestOptions, sourceLocale } from "virtual:verbaly";
5
+ //#region src/server.ts
6
+ const getRequestState = (React.cache ?? ((fn) => fn))(async () => {
7
+ const cookieName = requestOptions?.cookie ?? LOCALE_STORAGE_KEY;
8
+ const headerStore = await headers();
9
+ const locale = resolveRequestLocale({
10
+ supported: locales,
11
+ cookie: cookieName === false ? void 0 : (await cookies()).get(cookieName)?.value,
12
+ header: headerStore.get("accept-language"),
13
+ fallback: requestOptions?.fallback ?? sourceLocale
14
+ });
15
+ return {
16
+ locale,
17
+ instance: await createRequestInstance(locale)
18
+ };
19
+ });
20
+ async function getVerbaly() {
21
+ return (await getRequestState()).instance;
22
+ }
23
+ async function getT() {
24
+ return (await getRequestState()).instance.t;
25
+ }
26
+ async function getRequestLocale() {
27
+ return (await getRequestState()).locale;
28
+ }
29
+ async function getVerbalyProps() {
30
+ const { locale } = await getRequestState();
31
+ if (locale === sourceLocale) return { locale };
32
+ return {
33
+ locale,
34
+ messages: await loadMessages(locale)
35
+ };
36
+ }
37
+ //#endregion
38
+ export { getRequestLocale, getT, getVerbaly, getVerbalyProps };
39
+
40
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["import { cookies, headers } from 'next/headers';\nimport * as React from 'react';\nimport {\n LOCALE_STORAGE_KEY,\n resolveRequestLocale,\n type TFunction,\n type Verbaly,\n} from 'verbaly';\nimport {\n createRequestInstance,\n loadMessages,\n locales,\n requestOptions,\n sourceLocale,\n} from 'virtual:verbaly';\n\ninterface RequestState {\n locale: string;\n instance: Verbaly;\n}\n\n// React.cache = per-request dedupe in RSC; identity fallback keeps plain-node tests runnable\nconst perRequest: <T extends () => Promise<RequestState>>(fn: T) => T =\n (React as { cache?: <T>(fn: T) => T }).cache ?? ((fn) => fn);\n\nconst getRequestState = perRequest(async (): Promise<RequestState> => {\n const cookieName = requestOptions?.cookie ?? LOCALE_STORAGE_KEY;\n const headerStore = await headers();\n const cookieValue =\n cookieName === false ? undefined : (await cookies()).get(cookieName)?.value;\n const locale = resolveRequestLocale({\n supported: locales,\n cookie: cookieValue,\n header: headerStore.get('accept-language'),\n fallback: requestOptions?.fallback ?? sourceLocale,\n });\n return { locale, instance: await createRequestInstance(locale) };\n});\n\n// request-scoped instance — never the virtual:verbaly singleton (locale would leak between requests)\nexport async function getVerbaly(): Promise<Verbaly> {\n return (await getRequestState()).instance;\n}\n\nexport async function getT(): Promise<TFunction> {\n return (await getRequestState()).instance.t;\n}\n\nexport async function getRequestLocale(): Promise<string> {\n return (await getRequestState()).locale;\n}\n\nexport interface VerbalyProviderProps {\n locale: string;\n messages?: Record<string, string>;\n}\n\n// serializable props for <VerbalyProvider> — messages omitted when locale is the\n// source locale (it ships inline in the client bundle already)\nexport async function getVerbalyProps(): Promise<VerbalyProviderProps> {\n const { locale } = await getRequestState();\n if (locale === sourceLocale) return { locale };\n return { locale, messages: await loadMessages(locale) };\n}\n"],"mappings":";;;;;AAyBA,MAAM,mBAFH,MAAsC,WAAW,OAAO,IAAA,CAExB,YAAmC;CACpE,MAAM,aAAa,gBAAgB,UAAU;CAC7C,MAAM,cAAc,MAAM,QAAQ;CAGlC,MAAM,SAAS,qBAAqB;EAClC,WAAW;EACX,QAHA,eAAe,QAAQ,KAAA,KAAa,MAAM,QAAQ,EAAA,CAAG,IAAI,UAAU,CAAC,EAAE;EAItE,QAAQ,YAAY,IAAI,iBAAiB;EACzC,UAAU,gBAAgB,YAAY;CACxC,CAAC;CACD,OAAO;EAAE;EAAQ,UAAU,MAAM,sBAAsB,MAAM;CAAE;AACjE,CAAC;AAGD,eAAsB,aAA+B;CACnD,QAAQ,MAAM,gBAAgB,EAAA,CAAG;AACnC;AAEA,eAAsB,OAA2B;CAC/C,QAAQ,MAAM,gBAAgB,EAAA,CAAG,SAAS;AAC5C;AAEA,eAAsB,mBAAoC;CACxD,QAAQ,MAAM,gBAAgB,EAAA,CAAG;AACnC;AASA,eAAsB,kBAAiD;CACrE,MAAM,EAAE,WAAW,MAAM,gBAAgB;CACzC,IAAI,WAAW,cAAc,OAAO,EAAE,OAAO;CAC7C,OAAO;EAAE;EAAQ,UAAU,MAAM,aAAa,MAAM;CAAE;AACxD"}
package/loader.d.cts ADDED
@@ -0,0 +1,8 @@
1
+ // hand-written: the runtime is `module.exports = fn` (loader-runner contract);
2
+ // generated dts would declare an ESM default export and mismatch under node16
3
+ interface LoaderContext {
4
+ resourcePath: string;
5
+ async(): (error: unknown, code?: string, map?: unknown) => void;
6
+ }
7
+ declare function verbalyLoader(this: LoaderContext, source: string): void;
8
+ export = verbalyLoader;
package/package.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "@verbaly/next",
3
+ "version": "0.19.0",
4
+ "description": "Next.js integration for Verbaly — App Router/RSC with per-request locale negotiation, Turbopack and webpack support, flash-free hydration.",
5
+ "keywords": [
6
+ "i18n",
7
+ "next",
8
+ "nextjs",
9
+ "app-router",
10
+ "react-server-components",
11
+ "rsc",
12
+ "ssr",
13
+ "verbaly"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "Aron Soto",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/AronSoto/verbaly.git",
20
+ "directory": "packages/next"
21
+ },
22
+ "homepage": "https://verbaly-web.vercel.app/docs/frameworks/react",
23
+ "bugs": {
24
+ "url": "https://github.com/AronSoto/verbaly/issues"
25
+ },
26
+ "type": "module",
27
+ "main": "./dist/index.cjs",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "import": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "require": {
37
+ "types": "./dist/index.d.cts",
38
+ "default": "./dist/index.cjs"
39
+ }
40
+ },
41
+ "./server": {
42
+ "types": "./dist/server.d.ts",
43
+ "import": "./dist/server.js"
44
+ },
45
+ "./client": {
46
+ "types": "./dist/client.d.ts",
47
+ "import": "./dist/client.js"
48
+ },
49
+ "./loader": {
50
+ "types": "./loader.d.cts",
51
+ "default": "./dist/loader.cjs"
52
+ }
53
+ },
54
+ "files": [
55
+ "dist",
56
+ "loader.d.cts",
57
+ "LICENSE"
58
+ ],
59
+ "sideEffects": false,
60
+ "engines": {
61
+ "node": ">=20"
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ },
66
+ "dependencies": {
67
+ "@verbaly/compiler": "^0.19.0"
68
+ },
69
+ "peerDependencies": {
70
+ "next": "^15.0.0 || ^16.0.0",
71
+ "react": "^18.0.0 || ^19.0.0",
72
+ "verbaly": "^0.19.0",
73
+ "@verbaly/react": "^0.19.0"
74
+ },
75
+ "devDependencies": {
76
+ "@types/react": "^19.2.17",
77
+ "@types/react-dom": "^19.2.3",
78
+ "happy-dom": "^20.10.6",
79
+ "next": "^16.2.10",
80
+ "react": "^19.2.7",
81
+ "react-dom": "^19.2.7",
82
+ "@verbaly/react": "0.19.0",
83
+ "verbaly": "0.19.0"
84
+ },
85
+ "scripts": {
86
+ "build": "tsdown",
87
+ "dev": "tsdown --watch",
88
+ "test": "vitest run --passWithNoTests",
89
+ "typecheck": "tsc --noEmit"
90
+ }
91
+ }