From 90c68412ad47f26fcedcc413ad2baed1a6e42436 Mon Sep 17 00:00:00 2001 From: Menachem Hornbacher Date: Fri, 28 Nov 2025 16:07:42 -0500 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=93=9D=20docs(static-rendering):=20Ti?= =?UTF-8?q?mezone=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/src/pages/docs/routing/setup.mdx | 123 ++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/docs/src/pages/docs/routing/setup.mdx b/docs/src/pages/docs/routing/setup.mdx index 986660b2a..73c05577b 100644 --- a/docs/src/pages/docs/routing/setup.mdx +++ b/docs/src/pages/docs/routing/setup.mdx @@ -300,3 +300,126 @@ export async function generateMetadata({params}) { ``` + +### Timezone support + +When using static-rendering with locale-based routing, you might also want to consider the timezone of your users. Since the timezone is part of the users device settings, and therefore cannot be known at build time, it is optimally configured on the client side. + +To do this we'll setup the following files: + + + +### `src/i18n/useTimezone.ts` [#i18n-timezone-hook] + +We will create a simple React hook that can be used to get the users current timezone: + +```tsx filename="src/i18n/useClientTimeZone.ts" +'use client'; +import {useState, useEffect} from 'react'; + +/** Hook to get the users current timezone. */ +export function useClientTimeZone() { + // Initialize with the server timezone on the server render pass + const [timeZone, setTimeZone] = useState(getTimeZone()); + + useEffect(() => { + // Update the timezone to match the client during hydration + setTimeZone(getTimeZone()); + + // Optionally update timezone every minute to account for DST changes + const interval = setInterval(() => { + setTimeZone(getTimeZone()); + }, 60_000); + + return () => clearInterval(interval); + }, []); +} + +/** A simple utility function to get the current timezone on the Server or the Client */ +function getTimeZone() { + return Intl.DateTimeFormat().resolvedOptions().timeZone; +} +``` + +#### `src/i18n/provider.tsx` [#i18n-timezone-client-provider] + +Once we have that hook in place we can now create a Wrapper around `NextIntlClientProvider` that sets the timezone accordingly: + +```tsx filename="src/i18n/provider.tsx" +'use client'; + +import {NextIntlClientProvider} from 'next-intl'; +import {useClientTimeZone} from './useClientTimeZone'; + +// Props for the local Timezone-aware NextIntlClientProvider +type Props = Omit, 'timeZone'>; + +export function TZAwareNextIntlClientProvider(props: Props) { + const timeZone = useClientTimeZone(); + + return ; +} +``` + +#### `src/app/[locale]/layout.tsx` [#layout-timezone] + +Finally, we can use our `TZAwareNextIntlClientProvider` in our locale layout: + +```tsx filename="src/app/[locale]/layout.tsx" +import {getMessages, setRequestLocale} from 'next-intl/server'; // update this import +import {hasLocale} from 'next-intl'; +import {notFound} from 'next/navigation'; +import {routing} from '@/i18n/routing'; +import {TZAwareNextIntlClientProvider} from '@/i18n/provider'; // add this import + +type Props = { + children: React.ReactNode; + params: Promise<{locale: string}>; +}; + +export default async function LocaleLayout({children, params}: Props) { + // Ensure that the incoming `locale` is valid + const {locale} = await params; + if (!hasLocale(routing.locales, locale)) { + notFound(); + } + + // Enable static rendering + setRequestLocale(locale); + // Fetch the messages for the locale + const messages = await getMessages({locale}); + + // ... + + return ( + + {/* Wrap other providers who may use next-intl hooks here */} + {children} + + ); +} +``` + + + +
+Will my users experience visual "popping" when the timezone loads? + +When using the above setup, the initial server render will use the server's timezone. During hydration on the client, the timezone will be updated to match the user's device settings. This may lead to visual "popping" if there are differences between the two timezones (e.g., displaying dates or times). + +However, for dynamic components that are streamed in after hydration (e.g., via `Suspense`), the correct timezone will be used right away, preventing any popping in those parts of the UI. Considering that most date/time components are quite often dynamic (e.g., comments, timestamps, etc.), the overall impact of this popping is usually minimal. + +
+ +
+Will I experience hydration errors when the timezone loads? + +In most cases, you should not experience hydration errors when using the above setup. However there is a potential edge case to be aware of. If you are: + +- Using `useTranslations` or other client-side hooks outside of the children of `TZAwareNextIntlClientProvider` +- **_And_** you are formatting dates or times in those components. +- **_And_** the server timezone and client timezone differ. + +Then you will experience hydration errors. As such it is reccomended to wrap your entire application in a `TZAwareNextIntlClientProvider` in your `LocaleLayout` to avoid this issue. + +
From c5327d751c3d124c1a00028aa3cc5052b9998c49 Mon Sep 17 00:00:00 2001 From: Menachem Hornbacher Date: Fri, 28 Nov 2025 16:10:35 -0500 Subject: [PATCH 2/4] clarify cache components --- docs/src/pages/docs/routing/setup.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/pages/docs/routing/setup.mdx b/docs/src/pages/docs/routing/setup.mdx index 73c05577b..2d76bf060 100644 --- a/docs/src/pages/docs/routing/setup.mdx +++ b/docs/src/pages/docs/routing/setup.mdx @@ -407,7 +407,7 @@ export default async function LocaleLayout({children, params}: Props) { When using the above setup, the initial server render will use the server's timezone. During hydration on the client, the timezone will be updated to match the user's device settings. This may lead to visual "popping" if there are differences between the two timezones (e.g., displaying dates or times). -However, for dynamic components that are streamed in after hydration (e.g., via `Suspense`), the correct timezone will be used right away, preventing any popping in those parts of the UI. Considering that most date/time components are quite often dynamic (e.g., comments, timestamps, etc.), the overall impact of this popping is usually minimal. +However, for dynamic components that are streamed in after hydration (e.g., via `Suspense` with [Cache Components](https://nextjs.org/docs/app/getting-started/cache-components)), the correct timezone will be used right away, preventing any popping in those parts of the UI. Considering that most date/time components are quite often dynamic (e.g., comments, timestamps, etc.), the overall impact of this popping is usually minimal. From 5c6367291c2758e059339fb6ddee40803ba8d272 Mon Sep 17 00:00:00 2001 From: Menachem Hornbacher Date: Fri, 28 Nov 2025 16:14:54 -0500 Subject: [PATCH 3/4] typo: forgot to add return statement --- docs/src/pages/docs/routing/setup.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/pages/docs/routing/setup.mdx b/docs/src/pages/docs/routing/setup.mdx index 2d76bf060..406a17fa5 100644 --- a/docs/src/pages/docs/routing/setup.mdx +++ b/docs/src/pages/docs/routing/setup.mdx @@ -333,6 +333,8 @@ export function useClientTimeZone() { return () => clearInterval(interval); }, []); + + return timeZone; } /** A simple utility function to get the current timezone on the Server or the Client */ From ba943736879a420e084a024bab95b3ee1fa9cc92 Mon Sep 17 00:00:00 2001 From: Menachem Hornbacher Date: Fri, 28 Nov 2025 21:45:09 -0500 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=93=9D=20document=20new=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/src/pages/docs/routing/setup.mdx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/src/pages/docs/routing/setup.mdx b/docs/src/pages/docs/routing/setup.mdx index 406a17fa5..a6afb1fb2 100644 --- a/docs/src/pages/docs/routing/setup.mdx +++ b/docs/src/pages/docs/routing/setup.mdx @@ -404,6 +404,15 @@ export default async function LocaleLayout({children, params}: Props) { +
+Does this work for Server Components? + +Sadly it does not. Since the timezone is hydrated on the client, components without a `"use-client"` directive will still present using the server's timezone. + +When using Static Rendering / Cache Components we reccomend splitting out date/time related components into Client Components or marking your page with `"use client"`. Note that if you mark your entire page as a Client Component, you need to remove `setRequestLocale` from that page. + +
+
Will my users experience visual "popping" when the timezone loads?