# cloudflare-next-intl Optimized next-intl-style i18n for Next.js App Router, tuned for Cloudflare Workers deployment (tree-shakeable, per-subpath exports instead of one big barrel). Requires a `@intl-config` path alias pointing at a config file built via `setIntlConfig` — see `src/config/README.md` / README "Setup" — before any other subpath can be used. ## Subpaths - `.` — everything, re-exported (prefer the flat subpaths below for smaller bundles). - `./client` — client-side barrel: `LocaleLink`, `usePathname`, `setCookieClient`, `getCookieClient`. - `./server` — server-side barrel: `getMessage`, `getTranslations`, `getLocale`, `getCountry`, `getTimezone`, `resolveEnv`, `IntlProvider`, `Link`, `IntlHelperScript`, `getLocaleStaticParams`. - `./geo` / `./getCountry` / `./getTimezone` — country & timezone resolver helpers (`getCountry(input?, generate?, headerNames?)`, `getTimezone(input?, fallback?, generate?, headerNames?)`, `resolveEnv(generate?)`) for Vinext, OpenNext, and Cloudflare Workers. Custom header names can be configured via `generate.countryHeaderNames`/`generate.timezoneHeaderNames` or passed per-call. - `./middleware` — `intlMiddleware` for `middleware.ts`; locale detection/rewrite/redirect and automatic CF header forwarding. - `./setIntlConfig` — identity function for typed `RoutingConfig` authoring; use in your `@intl-config` file. - `./serverProvider` — `IntlProvider` server component (also under `./server`). - `./Link` — server-side locale-aware `` (also under `./server`). - `./IntlHelperScript` — inline script for flash-free locale/theme hydration. - `./LocaleLink` — client-side locale-aware link (counterpart to `./Link`). - `./usePathname` — client hook: pathname with locale prefix stripped. - `./metadata` — Next.js `generateMetadata` helper with locale alternates. - `./setCookieClient` / `./getCookieClient` — small client cookie utilities (not used internally for locale routing). - `./localeStaticParams` — `generateStaticParams` helper for locale segments. - `./use` — `useLocale`/`useTranslations`; resolves to the RSC or client implementation automatically via the `react-server` export condition. Both throw `"... must be used within an IntlProvider"` if called outside one. - `./ThemeSwitcher` — optional light/dark toggle component. - `./db` — `withPublicDb(fn)` / `withUserDb(fn, uid?)` server-side Postgres/Drizzle context helpers (require `db` set on your `RoutingConfig`; direct Postgres or Supabase Data API with automatic PostgREST REST translation and `cfni_exec` fallback, see below). - `./dbEslint` — flat-config ESLint fragment banning direct `@supabase/supabase-js`, `pg`, `postgres`, and deep `dist/` imports in application code. - `./dbHelpers` — generic Drizzle SQL helper functions (`excluded`, `onConflictSet`, `ago`, `currentDate`, `windowCount`, `unnestLateral`, `ascNullsLast`, `alwaysTrue`, `lateral`, `aliasColumn`, `minOf`, `maxOf`, `roundReal`, `multiply`, `scalarFromCte`) for use with `./db`. - `./vite` — `cloudflareNextIntl(options?)` / `cloudflareNextIntlPlugin`, `imageOptimizerPlugin(options?)` / `imageOptimizer`, `autoLocaleParamsPlugin(options?)`, `buildIdAsset(fileName?)`, `localeFilePlugin(options?)`, `userAgentStubPlugin()`, `cfWorkersClientStubPlugin()`, `vinextRouteWiringFixPlugin(options?)`, `lucideOptimizerPlugin(options?)`: All-in-one Vite plugin required for Vinext/Cloudflare Workers environments (bundles `@locale-file/*` via eager glob, resolves `@intl-config`, stubs Node.js `user-agent` to prevent runtime `node:fs` errors, stubs `cloudflare:workers` for client builds, emits client `BUILD_ID`, runs build-time/dev Image Optimizer with Next.js blur placeholder shimming, fixes vinext route wiring/matching/non-blocking optimistic prefetch learning, auto-inserts locale params for pages reading locale from cookies, and rewrites `lucide-react` imports to deep icon paths). Also wraps `checkDynamicPages`/`checkLocaleParams` (see `./checkDynamicPages` / `./checkLocaleParams`), running `autoLocaleParams` before `autoDynamicPages` so a page whose locale param this fixes no longer trips the cookie-derived-locale dynamic signal. - `./checkDynamicPages` — `checkDynamicPages(options, io?)`: standalone SSG-eligibility scanner/fixer for `page`/`route` files (text-heuristic, transitive local-import following, capped/cycle-safe). Modes `"off" | "report" | "fix"`. Also usable as the `cfni-check-dynamic-pages` CLI bin. - `./checkLocaleParams` — `checkLocaleParams(options, io?)`: standalone scanner/fixer for `page`/`layout`/`loading` files under a `[locale]`-scoped route that call `getTranslations()`/`useTranslations()` without a `locale` param or `setLocale()` call (the source of a hidden cookie-derived-locale dependency). Modes `"off" | "report" | "fix"`; options include `appDir`, `localeParam` (default `"locale"`), `skip`, `overrides`. Also usable as the `cfni-check-locale-params` CLI bin. - `./image-optimizer` / `./imageOptimizer` — image optimization suite: `imageOptimizerPlugin`, `imageOptimizer`, `resolveOptions`, `resolveImageConfig`, `resolveBlurOptions`, `processImage`, `makeBlurDataURL`, `getImageBlurSvg`, `renderManifest`, `writeManifest`, `isFresh`, `loadCache`, `saveCache`, `collectImages`, `run`. - `./errorHandling` — error reporting & stale deploy recovery barrel: `reportError`, `withErrorHandling`, `installConsoleErrorOverride`, `installGlobalErrorOverride`, `stringifyUnknown`, `formatErrorMessage`, `defaultIgnoredConsoleErrors`, `createServerErrorAction`, `isStaleDeployError`, `defaultStaleDeployPatterns`, `setStaleDeployPatterns`, `getStaleDeployPatterns`, `clearClientCache`, `useStaleDeployRecovery`, `shouldRecoverFromStaleDeploy`, `isRecentBuild`. - `./isStaleDeployError` — `isStaleDeployError(error, patterns?)`, `setStaleDeployPatterns(patterns)`, `getStaleDeployPatterns()`: detector returning `true` for version skew / chunk load / dynamic import / server action 404 / hydration errors (ChunkLoadError, UnrecognizedActionError, server action not found, failed to fetch, dynamically imported module failure, loading CSS chunk, connection closed, RSC payload failure, minified error #412, or missing stream error `undefined`) with fast pre-lowercased pattern cache and intl-config integration (`errorHandling.staleDeployPatterns`). - `./clearClientCache` — `clearClientCache()`: async helper wiping `window.caches`, unregistering service workers, and clearing `sessionStorage` for recovering from stale deployments. - `useStaleDeployRecovery(error, onRecover?, delayMs?)` (client hook, in `./errorHandling`) — once per build id (`localStorage['buildId']`, `sessionStorage` marker), waits `delayMs` (default 5000ms), runs optional `onRecover()` + `clearClientCache()` in parallel, then `window.location.reload()`. Returns whether a reload is pending, so caller renders a loading state instead of error UI. Recovers even past the one-reload cap when `localStorage['buildIdSetAt']` is <60s old (new deploy still settling). `shouldRecoverFromStaleDeploy(error, buildId, marker, recentBuild?)` and `isRecentBuild(setAt, now, windowMs?)` are the pure predicates. - `./createServerErrorAction` — `createServerErrorAction(action, config)`: wrapper for server actions with standardized error reporting. - `./reportClientError` — `reportClientError(error, classOrMethodName, params?)`: ready-made `"use server"` action reporting a client-originated error, config resolved via `@intl-config` (no per-app wrapper file or setup call needed, unlike `createServerErrorAction`). ## `firebaseAuth*` subpaths (require `firebaseAuth` set on your `RoutingConfig`) `@firebase/app`/`@firebase/auth`/`@firebase/app-check`/`@firebase/performance` are **peerDependencies** (install `firebase`, not the scoped packages directly, unless you already do) — never add them as a hard dependency of this package or a consumer's own package. `@firebase/app` holds its app registry as module-level state, so a second, independently-resolved copy silently starts its own untracked registry instead of joining the consumer's `getApps()`/`initializeApp()` — auth state then isn't shared. Peer resolution guarantees one shared copy instead. - `./firebaseAuthClient` — lazy Firebase client SDK getter. - `./firebaseAuthClientProvider` — `AuthUserProvider`: client auth-state provider (session cookie sync, auth-page redirects). - `./firebaseAuthServerProvider` — server-side equivalent provider (not used by the default auto-wiring path — see its doc comment). - `./useFirebaseAuthUser` — `useAuthUser()`; resolves to RSC or client implementation via the `react-server` condition. Client variant throws `"useAuthUser must be used within an AuthUserProvider"` if called outside one. - `./getFirebaseAuthUser` — `getAuthUser()`; unconditional server-only export of the same RSC implementation `useFirebaseAuthUser` resolves to via `react-server`. Use this when you want `await` to be visible from the type itself — TypeScript doesn't evaluate the `react-server` condition, so `useFirebaseAuthUser` always types as its client (sync) signature in editors regardless of call site. - `./firebaseAuthActions` — `createLoginAction`/`createSignUpAction`/`createForgotPasswordAction`: factories returning React `useActionState`-shaped form actions. `createSendSignInLinkAction`: same factory shape, for passwordless email-link sign-in — appends the user's email as an `email` query parameter to `actionCodeSettings.url` and returns `{ success: true, email }` so the caller can persist the trimmed email (e.g. to `localStorage`) for the completion step. `completeSignInWithLink(locale, url, email)`: plain async function (not `useActionState`-shaped) that completes a passwordless sign-in from the emailed link's landing page — call from an effect on mount, not a form submit. - `./firebaseAuthMiddleware` — `updateSession`: session-cookie refresh, called automatically by `./middleware`'s default handler. ## `cookieConsent*` subpaths (require `cookieConsent` set on your `RoutingConfig`) - `./cookieConsent` — barrel: `CookieConsentProvider`, `useCookieConsent`, `CookieConsentDialog`, `PrivacyPolicyUpdateDialog`, `CookieConsentAnalytics`. - `./CookieConsentProvider` — context provider; reads/writes consent + privacy-policy-date cookies. Auto-wired by `IntlProvider` when `cookieConsent` is configured — manual nesting is optional. - `./useCookieConsent` — context hook; throws `"useCookieConsent must be used within a CookieConsentProvider"` if called outside one. - `./CookieConsentDialog` — default cookie-consent banner; accepts per-slot `classNames`/`styles` or a `render` prop for fully custom markup. When `link` is omitted, renders a default link to `cookieConsent.privacyPolicyPath` (defaults to `'/privacy-policy'`; label via `privacyPolicyLinkText`, default `"Privacy Policy"`) — pass `link={null}` for no link, or set `privacyPolicyPath: false` to disable it everywhere. - `./PrivacyPolicyUpdateDialog` — "privacy policy updated" banner; auto-enabled only when `cookieConsent.privacyPolicyDate` is set. Same default-link behavior as `CookieConsentDialog` (label default `"Learn more"`). - `./cookieConsentAnalytics` — `CookieConsentAnalytics`: gates Cloudflare Web Analytics / Google Ads / Google Analytics / AdSense / Microsoft Clarity behind consent; rendered automatically by `IntlProvider` when `cookieConsent.secrets` or `getSecrets` is set (and `autoWireAnalytics !== false`). Never renders in local dev (`NODE_ENV === 'development'`) unless `cookieConsent.enableAnalyticsInDevMode` is `true`. - Country-based gating (`cookieConsent.countryHeaderNames` / `getCountryCode` / `getCloudflareContext` + `gdprCountries`): resolved server-side by `IntlProvider` into a `requiresConsent` boolean passed to `CookieConsentProvider`. Automatically falls back to request headers (`x-cf-country`, `cf-ipcountry` or custom `countryHeaderNames`) when neither getter is set or when they resolve empty. `getCountryCode` (direct country resolver) takes precedence over `getCloudflareContext` (reads `cf.country`) when both are set. Unresolved country (or a `null` context) always requires consent (fail-safe). `./cookieConsent` also exports `defaultGdprCountries` (EU/EEA + UK + Switzerland). ## `db*` subpaths (require `db` set on your `RoutingConfig`) Two transports, picked by which `db` fields are set — `pg`/`drizzle-orm`/`@supabase/supabase-js` ship as dependencies and load via dynamic `import()`, so nothing bundles unless a `db` export is called. - Direct Postgres (wins if configured): `db.connectionString` — Postgres connection string, or a sync/async function returning one (resolved on each connect). The function form is the way to read a value unavailable at module scope, e.g. a Cloudflare Hyperdrive binding: `connectionString: async () => (await getCloudflareContext({ async: true })).env.HYPERDRIVE.connectionString`. There is no separate `hyperdriveBinding` option. - `db.autoHyperdrive` — when `true` (default) and `db.connectionString` is unset, `env.HYPERDRIVE.connectionString` is read automatically before falling through to `db.supabase`. Set `false` to skip this and go straight to `supabase` (or the "no connection string" error). - `db.autoHyperdriveSkipUrls` — connection strings from `env.HYPERDRIVE.connectionString` treated as "no connection" by auto-Hyperdrive (e.g. `wrangler dev`'s unconfigured placeholder). Defaults to `['postgresql://user:pass@localhost:5432/db']`. - Supabase Data API (used only when neither of the above is set): `db.supabase` — `{ url?, anonKey?, execFunction?, rawSql? }` where `url`/`anonKey` each accept a string or a sync/async function returning one, defaulting `url`/`anonKey` to `NEXT_PUBLIC_SUPABASE_URL`/`NEXT_PUBLIC_SUPABASE_ANON_KEY`. Statements are translated to PostgREST REST calls first; unsupported statements fall back to `supabase/cfni_exec.sql` (a `security invoker` SQL-exec function) in your database — `cfni-db-codegen`/`cfni-db-install-exec` can install it for you (see below). No multi-statement transactions — each statement in a `withUserDb` callback is its own round-trip; `.transaction()` throws instead of running non-atomically. `rawSql: false` disables `cfni_exec` fallback, throwing an informative error when a query cannot be served over REST. - `db.disconnectAfterRequest` — deprecated, ignored since 0.8.23. Each `withPublicDb`/`withUserDb` call opens and closes its own client; Hyperdrive pools the server-side connection. - `db.authenticatedRole` — direct-Postgres mode only: Postgres role assumed inside `withUserDb`'s transaction. Accepts a string or a sync/async function returning one. Defaults to `'authenticated'` (Supabase RLS convention). - `db.authenticatedRoleClaim` — direct-Postgres mode only: Firebase custom claim field (default `'role'`) read to determine the Postgres role in `withUserDb` when `firebaseAuth` is enabled. Set `false` to disable. - `db.getUserId` — direct-Postgres mode only: resolves the user id injected as `request.jwt.claims->>'sub'` in `withUserDb`. Omit when `firebaseAuth` is configured — the uid is then taken automatically from the signed-in Firebase user via this package's own `getAuthUser()`. - `db.getAccessToken` — Supabase mode only: resolves the JWT sent as `Authorization: Bearer` in `withUserDb`, which is what makes PostgREST resolve `authenticated` and apply RLS. Omit when `firebaseAuth` is configured — the signed-in user's Firebase ID token is used automatically. - `db.disconnectTimeoutMs` — deprecated, ignored since 0.8.23. Teardown is awaited or deferred to `ctx.waitUntil` without a timeout. - `withPublicDb(fn)` — anonymous role. Direct-Postgres mode: the request's pooled connection, no transaction, no role switch. Supabase mode: the anon key as the PostgREST bearer token. Either way, no user id is attached — RLS keyed on `auth.jwt()` denies access. - `withUserDb(fn, uid?)` — signed-in-user role. Direct-Postgres mode: a transaction with `set_config('request.jwt.claims', ...)` + `set local role`, `uid` resolution order explicit arg → `db.getUserId()` → Firebase auth uid → throws. Supabase mode: identity rides on the JWT from `db.getAccessToken`/Firebase instead (`uid` param is ignored), no transaction wraps the call. - `./dbHelpers` functions are plain Drizzle `sql`-building utilities with no config dependency — usable standalone. - `cfni-db-codegen` binary — regenerates Drizzle models via `drizzle-kit pull`. By default, prefers `embedded-postgres` (zero setup, no Docker needed) to load DDL from `--ddl-dir` and introspect it. If an explicit `--db-url`/`CODEGEN_DATABASE_URL` is set, connects to that live DB (and falls back to `embedded-postgres` with a warning if unreachable). Flags/env: `--ddl-dir`/`CFNI_DB_DDL_DIR`, `--out-dir`/`CFNI_DB_OUT_DIR`, `--out-file`/`CFNI_DB_OUT_FILE`, `--db-url`/`CODEGEN_DATABASE_URL`, `--drizzle-config`/`CFNI_DB_DRIZZLE_CONFIG`, `--rpc-dir`/`CFNI_DB_RPC_DIR`, `--tests-dir`/`CFNI_DB_TESTS_DIR`, `--force`/`CFNI_DB_FORCE_EXEC`, `--skip-exec`/`CFNI_DB_SKIP_EXEC`, `--check`. `--out-dir` is repeatable and accepts a comma-separated list, so one run generates the same schema (and `manifest.json`) into several projects; `--check` verifies every target. After a successful run it also copies `supabase/cfni_exec.sql` + its pgTAP test file into `--rpc-dir`/`--tests-dir` (siblings of `--ddl-dir` by default), gated on `db.supabase.rawSql` (read from `next.config.*`'s `@intl-config` alias; unknown → warns and assumes `true`) — skips if `rawSql: false`, skips if `--skip-exec`, skips-with-warning on a differing existing file unless `--force`. `cfni-db-install-exec` runs only this copy step, same flags, no Postgres/drizzle-kit needed. ```typescript // src/i18n/intl_config.ts import { getCloudflareContext } from "@opennextjs/cloudflare"; export default setIntlConfig({ locales: ["en", "uk"] as const, defaultLocale: "en", generate: { getCloudflareContext }, db: { connectionString: async () => (await getCloudflareContext({ async: true })).env.HYPERDRIVE .connectionString, }, }); // anywhere on the server import { withPublicDb } from "cloudflare-next-intl/db"; const rows = await withPublicDb((db) => db.select().from(bonds).limit(10)); ``` ## Conventions - Every exported function/component has a JSDoc comment with an `@example` where usage isn't obvious from the signature alone. - Errors from missing-provider hooks name the hook and the required provider explicitly (e.g. `"useAuthUser must be used within an AuthUserProvider"`). - One exported concept per file/subpath; `exports` in `package.json` is the source of truth for what's public — `src/**/index.ts` barrels are internal except `./client` and `./server`, which are themselves public subpaths. ## Gotchas - Missing `@intl-config` alias (see README "Setup") throws at module-load time, before any of your own code runs. - Missing `firebaseAuth` on `RoutingConfig` throws immediately from any `firebaseAuth*` export, naming the missing config field. - `useLocale`/`useTranslations` (from `./use`) and `useAuthUser` (from `./useFirebaseAuthUser`) resolve to a Server- or Client-Component implementation automatically via the `react-server` export condition — always import from the subpath, never from an internal file path. - No unified `useFirebaseAuthUser` re-export inside `cloudflare-next-intl/firebaseAuth`'s barrel — that barrel can't replicate the `react-server` condition split, so import `cloudflare-next-intl/useFirebaseAuthUser` directly for the hook. - Missing `db` on `RoutingConfig` throws immediately from `withPublicDb`/`withUserDb`, naming the missing config field. `pg`, `drizzle-orm`, and `@supabase/supabase-js` ship as dependencies and load via dynamic `import()` — nothing to install yourself, and nothing bundles unless a `db` export is called. - In Supabase mode, statements are automatically translated to PostgREST REST calls; unsupported statements fall back to `cfni_exec`. Setting `db.supabase.rawSql: false` disables `cfni_exec` fallback and throws for untranslatable queries. ## Instructions for AI Agents / Coding Assistants When setting up or writing code for a project using `cloudflare-next-intl`: 1. **Path Alias Setup (`@intl-config`)**: - `tsconfig.json`: `"paths": { "@intl-config": ["./src/i18n/intl_config.ts"] }` - `next.config.mjs`: `webpack: (config) => { config.resolve.alias['@intl-config'] = path.resolve('./src/i18n/intl_config.ts'); return config; }` 2. **Configuration (`src/i18n/intl_config.ts`)**: - Use `setIntlConfig({ locales: ['en', ...], defaultLocale: 'en', db: { ... } })` 3. **Database Rules (Strict)**: - **DO NOT** install or import `@supabase/supabase-js`, `pg`, `postgres`, or `drizzle-orm` directly. - **Import query runners from `cloudflare-next-intl/db`**: `withPublicDb((db) => ...)` (anon role), `withUserDb((db) => ...)` (authenticated user role with RLS). - **Import schema builders from `cloudflare-next-intl/dbSchema`**: `pgTable`, `text`, `integer`, `timestamp`, `uuid`, `varchar`, `sql`, etc. - **Import query helpers from `cloudflare-next-intl/dbHelpers`**: `eq`, `and`, `or`, `inArray`, `notInArray`, `count`, `desc`, `asc`, etc. - **Add ESLint enforcement**: In `eslint.config.js`, spread `dbEslint` from `cloudflare-next-intl/dbEslint`. 4. **Auth Rules**: - For client and server auth state, import `useAuthUser` from `cloudflare-next-intl/useFirebaseAuthUser`. - Wrap client component root with `AuthUserProvider` from `cloudflare-next-intl/firebaseAuthClientProvider` (or use auto-wired `IntlProvider`). 5. **Codegen**: - Run `npx cfni-db-codegen` to pull Postgres schemas into typesafe Drizzle models using `cloudflare-next-intl/dbSchema`.