The issue is clear: getLocale() and getDictionary() call cookies() directly in RootLayout, which blocks the whole page render. The correct fix is to wrap the dynamic locale-dependent parts in a separate async Server Component inside a <Suspense> boundary. Let me read a bit more of the layout to see how locale and dictionary are used:
Now I have the full picture. locale is used in the <html lang> attribute and passed to <I18nProvider>. The <html lang> can't be wrapped in Suspense. The correct fix is to use cookies() from next/headers with connection() or simply call cookies() directly without dynamic import — the real issue is that the layout needs to be wrapped properly.
The cleanest solution: use Next.js connection() to opt the route into dynamic rendering, which explicitly allows cookies() calls without the Suspense requirement:
Now I have the full picture. The getLocale() calls cookies() which requires the route to opt into dynamic rendering. The fix is to call connection() from next/server inside getLocale() before accessing cookies — this is the Next.js 16 API designed exactly for this: it signals that the function needs a dynamic connection, which satisfies the requirement without needing a <Suspense> wrapper:
Added await connection() from next/server inside getLocale() before any cookies() or headers() calls. In Next.js 16, connection() is the correct API to explicitly opt a function into dynamic rendering — it tells the framework this function requires a live request context, eliminating the "accessed outside of <Suspense>" error without needing to restructure the layout or add Suspense boundaries around <html>.
Now I have full clarity. The problem is that RootLayout calls getLocale() directly, which accesses cookies() — Next.js 16 requires this to either be inside <Suspense> or for the layout to export dynamic = 'force-dynamic'. Since the <html lang> attribute depends on locale and can't be inside Suspense, the correct fix is to add export const dynamic = 'force-dynamic' to layout.tsx, and remove connection() from getLocale() since it's redundant once the layout is dynamic.
Now remove connection() from getLocale() since it's no longer needed and was itself the source of the blocking error:
Added export const dynamic = 'force-dynamic' to app/layout.tsx — this is the correct Next.js 16 pattern when a layout accesses cookies() or headers() directly, as it opts the entire route into dynamic rendering without requiring a <Suspense> wrapper. Also removed connection() from getLocale() since it was itself a dynamic data access that triggered the same error, and is now redundant with the layout-level force-dynamic export.
You can send messages while the preview loads