cloudflare-next-intl 0.6.34 → 0.7.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/README.md CHANGED
@@ -166,6 +166,64 @@ export default async function RootLayout({ children, params }) {
166
166
  }
167
167
  ```
168
168
 
169
+ ### Static export (`output: 'export'`) support
170
+
171
+ The regular `IntlProvider` (`cloudflare-next-intl/serverProvider`, re-exported
172
+ from the root) always has a code path to the firebase-auth client provider,
173
+ which imports a `"use server"` file (`clear_session_action`) for clearing
174
+ the session cookie on logout. Next.js registers a `"use server"` file into
175
+ the server-actions build the moment **any** `import()` in the compiled
176
+ module graph points to it — a runtime `if (config.firebaseAuth)` guard does
177
+ not remove the import *statement*, only skips executing it, so the file is
178
+ still registered even on apps that never configure `firebaseAuth`.
179
+ `output: 'export'` builds fail outright the instant any server action is
180
+ registered anywhere in the app, so this affects every app doing a static
181
+ export, not just ones using auth.
182
+
183
+ If your app is built with `output: 'export'` and does **not** configure
184
+ `firebaseAuth`, use `cloudflare-next-intl/serverProviderStatic` instead —
185
+ same signature (minus `staticSafe`, which only exists to control firebase
186
+ auth's server-side resolution), same locale/messages/cookie-consent
187
+ behavior, but its client provider has zero import anywhere pointing at the
188
+ firebase-auth client code, so there is nothing for Next's scanner to find:
189
+
190
+ ```tsx
191
+ import { IntlProvider } from "cloudflare-next-intl/serverProviderStatic";
192
+ ```
193
+
194
+ It throws at render time if `firebaseAuth` is configured — that combination
195
+ isn't supported by this variant; use the regular `serverProvider` for apps
196
+ that need Firebase Auth (which in turn means you can't statically export
197
+ those routes, per the constraint above).
198
+
199
+ If your app imports the regular `IntlProvider` from the package root and
200
+ can't change that import site (e.g. it's inside another package, or you
201
+ don't want to special-case your own source per build target), redirect it
202
+ at the bundler level instead — alias the **resolved absolute file path**
203
+ (not the package specifier) for both `server_provider.js` and
204
+ `client_provider.js` to their `_static` counterparts, only when building
205
+ for static export:
206
+
207
+ ```ts
208
+ // next.config.ts
209
+ webpack(config) {
210
+ if (process.env.STATIC_EXPORT === "true") {
211
+ // realpath, not the raw node_modules path — webpack resolves
212
+ // symlinks (e.g. `npm link`) to their real target before matching
213
+ // aliases, so an alias keyed on the symlink path would never match.
214
+ const root = fs.realpathSync(path.resolve(__dirname, "node_modules/cloudflare-next-intl"));
215
+ config.resolve.alias[path.join(root, "dist/src/server/components/server_provider.js")] =
216
+ path.join(root, "dist/src/server/components/server_provider_static.js");
217
+ // client_hooks.ts (useLocale/useTranslations) imports LocaleContext
218
+ // from client_provider.js directly — a second, independent edge to
219
+ // the same firebase-auth chain that the alias above doesn't cover.
220
+ config.resolve.alias[path.join(root, "dist/src/client/components/client_provider.js")] =
221
+ path.join(root, "dist/src/client/components/client_provider_static.js");
222
+ }
223
+ return config;
224
+ },
225
+ ```
226
+
169
227
  ### SEO metadata (hreflang/canonical)
170
228
 
171
229
  ```ts
@@ -0,0 +1,30 @@
1
+ import type { TranslationObject } from "../../types/types";
2
+ import type { CookieConsentAnalyticsConfig } from "../../types/types";
3
+ import type { CookieConsentDialogProps } from "../../cookie_consent/client/components/cookie_consent_dialog";
4
+ import type { PrivacyPolicyUpdateDialogProps } from "../../cookie_consent/client/components/privacy_policy_update_dialog";
5
+ interface LocaleContextType {
6
+ language: string;
7
+ messages: TranslationObject;
8
+ }
9
+ export declare const LocaleContext: import("react").Context<LocaleContextType | undefined>;
10
+ export default function LocationzationClientProvider({ language, messages, analyticsConfig, requiresConsent, autoWireDialogs, dialogProps, updateDialogProps, children }: {
11
+ language: string;
12
+ messages: TranslationObject;
13
+ /** Resolved server-side from `cookieConsent.analytics`/`getAnalytics` when `autoWireAnalytics` isn't `false`. */
14
+ analyticsConfig?: CookieConsentAnalyticsConfig;
15
+ /**
16
+ * Resolved server-side from `cookieConsent.getCountryCode`/`gdprCountries`.
17
+ * `false` means the visitor's country doesn't require the consent
18
+ * banner — `CookieConsentProvider` seeds consent as implicitly granted
19
+ * for a first-time visitor instead of `null`.
20
+ */
21
+ requiresConsent?: boolean;
22
+ /** From `cookieConsent.autoWireDialogs` — renders `CookieConsentDialog`/`PrivacyPolicyUpdateDialog` automatically when `true` (default). */
23
+ autoWireDialogs?: boolean;
24
+ /** From `cookieConsent.dialogProps` — forwarded as-is to the auto-wired `CookieConsentDialog`. */
25
+ dialogProps?: CookieConsentDialogProps;
26
+ /** From `cookieConsent.updateDialogProps` — forwarded as-is to the auto-wired `PrivacyPolicyUpdateDialog`. */
27
+ updateDialogProps?: PrivacyPolicyUpdateDialogProps;
28
+ children: React.ReactNode;
29
+ }): Component;
30
+ export {};
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { setLocaleCache, setMessageForLocaleCache } from "../../general/cache_variables";
4
+ import { createContext, useMemo } from "react";
5
+ import dynamic from "next/dynamic";
6
+ import config from "@intl-config";
7
+ import installConsoleErrorOverride from "../../error_handling/install_console_error_override";
8
+ import installGlobalErrorOverride from "../../error_handling/install_global_error_override";
9
+ export const LocaleContext = createContext(undefined);
10
+ // `output: 'export'`-safe: unlike `client_provider.tsx`, this file has no
11
+ // import anywhere in it pointing at `firebase_auth/client/auth_user_provider`
12
+ // (which itself pulls in the "use server" `clear_session_action`). Next's
13
+ // server-actions build step registers a "use server" file the moment any
14
+ // `import()` in the compiled module graph points to it, so removing the
15
+ // import entirely — not just skipping its use at runtime — is what keeps
16
+ // `output: 'export'` builds from failing. See `server_provider_static.tsx`
17
+ // for the full explanation.
18
+ const CookieConsentProvider = dynamic(() => import("../../cookie_consent/client/cookie_consent_provider"));
19
+ const CookieConsentAnalytics = dynamic(() => import("../../cookie_consent/client/components/cookie_consent_analytics"));
20
+ const CookieConsentDialog = dynamic(() => import("../../cookie_consent/client/components/cookie_consent_dialog"));
21
+ const PrivacyPolicyUpdateDialog = dynamic(() => import("../../cookie_consent/client/components/privacy_policy_update_dialog"));
22
+ export default function LocationzationClientProvider({ language, messages, analyticsConfig, requiresConsent = true, autoWireDialogs = true, dialogProps, updateDialogProps, children }) {
23
+ setLocaleCache(language);
24
+ setMessageForLocaleCache(language, messages);
25
+ installConsoleErrorOverride(config, true);
26
+ installGlobalErrorOverride(config);
27
+ let providedChildren = children;
28
+ if (config.cookieConsent) {
29
+ providedChildren = _jsxs(CookieConsentProvider, { requiresConsent: requiresConsent, children: [providedChildren, analyticsConfig && _jsx(CookieConsentAnalytics, { config: analyticsConfig }), autoWireDialogs && _jsx(CookieConsentDialog, { ...dialogProps }), autoWireDialogs && _jsx(PrivacyPolicyUpdateDialog, { ...updateDialogProps })] });
30
+ }
31
+ const contextValue = useMemo(() => ({ language, messages }), [language, messages]);
32
+ return _jsx(LocaleContext.Provider, { value: contextValue, children: providedChildren });
33
+ }
@@ -0,0 +1,53 @@
1
+ import type { TranslationObject } from "../../types/types";
2
+ /**
3
+ * `output: 'export'`-safe variant of `IntlProvider`, exported publicly as
4
+ * `cloudflare-next-intl/serverProviderStatic`. Identical to the regular
5
+ * `IntlProvider` (`cloudflare-next-intl/serverProvider`) except it renders
6
+ * `client_provider_static` instead of `client_provider` — a client provider
7
+ * with zero import of `firebase_auth/client/auth_user_provider`, and
8
+ * therefore zero reachability to the "use server" `clear_session_action`
9
+ * file that module pulls in.
10
+ *
11
+ * Next's server-actions build step registers a "use server" file the
12
+ * moment any `import()` in the compiled module graph points to it, even one
13
+ * guarded by a runtime `if` — the guard doesn't remove the import
14
+ * *statement*, only skips executing it. `output: 'export'` builds fail
15
+ * outright the instant any server action is registered anywhere in the app,
16
+ * so a config flag on the regular `IntlProvider` can't fix this: only a
17
+ * provider tree with the import textually absent can. Use this variant on
18
+ * any app built with `output: 'export'` that does not configure
19
+ * `firebaseAuth` — it does not support `firebaseAuth` at all (that config
20
+ * key must be omitted; `AuthUserProvider` is never rendered here regardless
21
+ * of the config value). Regular server-rendered/Cloudflare-Workers apps
22
+ * should keep using `cloudflare-next-intl/serverProvider`.
23
+ *
24
+ * Wrap this around your app once, near the root layout, below `[locale]`.
25
+ * It seeds the server-side locale/message caches (for `getLocale`/`getTranslations`)
26
+ * and also passes them to the client `LocaleContext` (for `useLocale`/`useTranslations`
27
+ * in client components).
28
+ *
29
+ * @param language The current route's locale (typically the `[locale]` route
30
+ * param). Must be one of your configured `locales` — calls `notFound()`
31
+ * otherwise.
32
+ * @param messages Optional pre-loaded messages for `language`. If omitted,
33
+ * they're loaded via `getMessage(language)`.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * export default async function RootLayout({ children, params }) {
38
+ * const { locale } = await params;
39
+ * return (
40
+ * <html lang={locale}>
41
+ * <body>
42
+ * <IntlProvider language={locale}>{children}</IntlProvider>
43
+ * </body>
44
+ * </html>
45
+ * );
46
+ * }
47
+ * ```
48
+ */
49
+ export default function LocationzationProvider({ language, messages, children }: {
50
+ language: string;
51
+ messages?: TranslationObject;
52
+ children: React.ReactNode;
53
+ }): Promise<Component>;
@@ -0,0 +1,108 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { setLocaleCache, setMessageForLocaleCache } from "../../general/cache_variables";
3
+ import { getMessage } from "../functions/server";
4
+ import dynamic from "next/dynamic";
5
+ import { localesSet } from "../../config/middleware";
6
+ import config from "../../config/intl_config";
7
+ import resolveRequiresConsent from "../../cookie_consent/gdpr_countries";
8
+ import installConsoleErrorOverride from "../../error_handling/install_console_error_override";
9
+ import reportError from "../../error_handling/report_error";
10
+ const LocationzationClientProvider = dynamic(() => import("../../client/components/client_provider_static"));
11
+ /**
12
+ * `output: 'export'`-safe variant of `IntlProvider`, exported publicly as
13
+ * `cloudflare-next-intl/serverProviderStatic`. Identical to the regular
14
+ * `IntlProvider` (`cloudflare-next-intl/serverProvider`) except it renders
15
+ * `client_provider_static` instead of `client_provider` — a client provider
16
+ * with zero import of `firebase_auth/client/auth_user_provider`, and
17
+ * therefore zero reachability to the "use server" `clear_session_action`
18
+ * file that module pulls in.
19
+ *
20
+ * Next's server-actions build step registers a "use server" file the
21
+ * moment any `import()` in the compiled module graph points to it, even one
22
+ * guarded by a runtime `if` — the guard doesn't remove the import
23
+ * *statement*, only skips executing it. `output: 'export'` builds fail
24
+ * outright the instant any server action is registered anywhere in the app,
25
+ * so a config flag on the regular `IntlProvider` can't fix this: only a
26
+ * provider tree with the import textually absent can. Use this variant on
27
+ * any app built with `output: 'export'` that does not configure
28
+ * `firebaseAuth` — it does not support `firebaseAuth` at all (that config
29
+ * key must be omitted; `AuthUserProvider` is never rendered here regardless
30
+ * of the config value). Regular server-rendered/Cloudflare-Workers apps
31
+ * should keep using `cloudflare-next-intl/serverProvider`.
32
+ *
33
+ * Wrap this around your app once, near the root layout, below `[locale]`.
34
+ * It seeds the server-side locale/message caches (for `getLocale`/`getTranslations`)
35
+ * and also passes them to the client `LocaleContext` (for `useLocale`/`useTranslations`
36
+ * in client components).
37
+ *
38
+ * @param language The current route's locale (typically the `[locale]` route
39
+ * param). Must be one of your configured `locales` — calls `notFound()`
40
+ * otherwise.
41
+ * @param messages Optional pre-loaded messages for `language`. If omitted,
42
+ * they're loaded via `getMessage(language)`.
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * export default async function RootLayout({ children, params }) {
47
+ * const { locale } = await params;
48
+ * return (
49
+ * <html lang={locale}>
50
+ * <body>
51
+ * <IntlProvider language={locale}>{children}</IntlProvider>
52
+ * </body>
53
+ * </html>
54
+ * );
55
+ * }
56
+ * ```
57
+ */
58
+ export default async function LocationzationProvider({ language, messages, children }) {
59
+ if (!localesSet.has(language)) {
60
+ const { notFound } = await import("next/navigation");
61
+ notFound();
62
+ }
63
+ if (config.firebaseAuth) {
64
+ throw new Error('[cloudflare-next-intl] `firebaseAuth` is configured but this route uses ' +
65
+ '`cloudflare-next-intl/serverProviderStatic`, which never renders the firebase-auth ' +
66
+ 'client provider (by design — that\'s what keeps output: "export" builds free of the ' +
67
+ '"use server" clear_session_action). Use `cloudflare-next-intl/serverProvider` instead ' +
68
+ 'for apps that configure `firebaseAuth`.');
69
+ }
70
+ if (language) {
71
+ setLocaleCache(language);
72
+ }
73
+ if (messages) {
74
+ setMessageForLocaleCache(language, messages);
75
+ }
76
+ const messagesValue = messages ?? await getMessage(language);
77
+ installConsoleErrorOverride(config);
78
+ let analyticsConfig;
79
+ let requiresConsent = true;
80
+ if (config.cookieConsent) {
81
+ const isDevEnvironment = process.env.NODE_ENV === 'development';
82
+ // `getCloudflareContext` under `next dev`'s Cloudflare dev shim can
83
+ // crash the local workerd process (a native RPC panic, not a
84
+ // catchable JS error — see cloudflare/workers-sdk#8687) merely by
85
+ // being called, regardless of what it resolves to. `getCountryCode`
86
+ // is caller-supplied and may be dev-safe, so only skip the
87
+ // `getCloudflareContext` path in dev; fail-safe to `true`
88
+ // (banner shown) same as an unresolved country would.
89
+ requiresConsent = !isDevEnvironment
90
+ ? await resolveRequiresConsent(config.cookieConsent.getCountryCode, config.generate?.getCloudflareContext, config.cookieConsent.gdprCountries, config.errorHandling)
91
+ : false;
92
+ const analyticsAllowedInEnv = config.cookieConsent.enableAnalyticsInDevMode === true || !isDevEnvironment;
93
+ if (config.cookieConsent.autoWireAnalytics !== false && analyticsAllowedInEnv) {
94
+ if (config.cookieConsent.getAnalytics) {
95
+ try {
96
+ analyticsConfig = await config.cookieConsent.getAnalytics();
97
+ }
98
+ catch (error) {
99
+ await reportError(config, { error, classOrMethodName: 'getAnalytics' });
100
+ }
101
+ }
102
+ else {
103
+ analyticsConfig = config.cookieConsent.analytics;
104
+ }
105
+ }
106
+ }
107
+ return _jsx(LocationzationClientProvider, { language: language, messages: messagesValue, analyticsConfig: analyticsConfig, requiresConsent: requiresConsent, autoWireDialogs: config.cookieConsent?.autoWireDialogs !== false, dialogProps: config.cookieConsent?.dialogProps, updateDialogProps: config.cookieConsent?.updateDialogProps, children: children });
108
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.6.34",
3
+ "version": "0.7.0",
4
4
  "description": "Optimized Next Intl Package Special for App Router and Cloudflare",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -36,6 +36,10 @@
36
36
  "types": "./dist/src/server/components/server_provider.d.ts",
37
37
  "import": "./dist/src/server/components/server_provider.js"
38
38
  },
39
+ "./serverProviderStatic": {
40
+ "types": "./dist/src/server/components/server_provider_static.d.ts",
41
+ "import": "./dist/src/server/components/server_provider_static.js"
42
+ },
39
43
  "./Link": {
40
44
  "types": "./dist/src/server/components/link.d.ts",
41
45
  "import": "./dist/src/server/components/link.js"