Skip to content

Commit

Permalink
feat!: Automatically inherit formats when NextIntlClientProvider
Browse files Browse the repository at this point in the history
…is rendered from a Server Component (#1191)

This should ease the transition from Server to Client Components, as you
don't have to manually pass this prop anymore. If you've previously
passed this prop manually, you can remove this assignment now.

If this is not desired (e.g. because you have a large `formats` object
that you don't want to pass to the client side), you can manually
opt-out via `formats={{}}` on `NextIntlClientProvider` in order to not
provide any formats on the client side.

**BREAKING CHANGE:** There's a very rare chance where this can break
existing behavior. If you're rendering `NextIntlClientProvider` in a
Server Component, you rely on static rendering, but you're not using
`unstable_setRequestLocale` (i.e. you're using hooks like
`useTranslations` exclusively in Client Components), this can opt your
page into dynamic rendering. If this affects you, please provide the
`formats` prop explicitly to `NextIntlClientProvider`.
  • Loading branch information
amannn authored Aug 26, 2024
1 parent 7c6f533 commit 8b4c7c4
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 10 deletions.
2 changes: 1 addition & 1 deletion docs/pages/docs/environments/server-client-components.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ In particular, page and search params are often a great option because they offe

### Option 3: Providing individual messages

To reduce bundle size, `next-intl` doesn't automatically provide [messages](/docs/usage/configuration#messages) or [formats](/docs/usage/configuration#formats) to Client Components.
To reduce bundle size, `next-intl` doesn't automatically provide [messages](/docs/usage/configuration#messages) to Client Components.

If you need to incorporate dynamic state into components that can not be moved to the server side, you can wrap these components with `NextIntlClientProvider` and provide the relevant messages.

Expand Down
12 changes: 8 additions & 4 deletions docs/pages/docs/usage/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ These props are inherited if you're rendering `NextIntlClientProvider` from a Se
1. `locale`
2. `now`
3. `timeZone`
4. `formats`

In contrast, these props can be provided as necessary:

1. `messages` (see [Internationalization in Client Components](/docs/environments/server-client-components#using-internationalization-in-client-components))
2. `formats`
3. `defaultTranslationValues`
4. `onError` and `getMessageFallback`
2. `defaultTranslationValues`
3. `onError` and `getMessageFallback`

<Details id="nextintlclientprovider-non-serializable-props">
<summary>How can I provide non-serializable props like `onError` to `NextIntlClientProvider`?</summary>
Expand Down Expand Up @@ -130,9 +130,9 @@ export default function IntlProvider({
locale={locale}
now={now}
timeZone={timeZone}
formats={formats}
// Provide as necessary
messages={messages}
formats={formats}
/>
);
}
Expand All @@ -143,6 +143,7 @@ Once you have defined your client-side provider component, you can use it in a S
```tsx filename="layout.tsx"
import IntlProvider from './IntlProvider';
import {getLocale, getNow, getTimeZone, getMessages} from 'next-intl/server';
import formats from './formats';

export default async function RootLayout({children}) {
const locale = await getLocale();
Expand All @@ -158,6 +159,7 @@ export default async function RootLayout({children}) {
now={now}
timeZone={timeZone}
messages={messages}
formats={formats}
>
{children}
</NextIntlClientProvider>
Expand Down Expand Up @@ -506,6 +508,8 @@ function Component() {
}
```

Formats are automatically inherited from the server side if you wrap the relevant components in a `NextIntlClientProvider` that is rendered by a Server Component.

## Default translation values

To achieve consistent usage of translation values and reduce redundancy, you can define a set of global default values. This configuration can also be used to apply consistent styling of commonly used rich text elements.
Expand Down
2 changes: 1 addition & 1 deletion packages/next-intl/.size-limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const config: SizeLimitConfig = [
},
{
path: 'dist/production/index.react-server.js',
limit: '14.665 KB'
limit: '14.675 KB'
},
{
path: 'dist/production/navigation.react-client.js',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {expect, it, vi} from 'vitest';
import getFormats from '../server/react-server/getFormats';
import {getLocale, getNow, getTimeZone} from '../server.react-server';
import NextIntlClientProvider from '../shared/NextIntlClientProvider';
import NextIntlClientProviderServer from './NextIntlClientProviderServer';
Expand All @@ -9,6 +10,16 @@ vi.mock('../../src/server/react-server', async () => ({
getTimeZone: vi.fn(async () => 'America/New_York')
}));

vi.mock('../../src/server/react-server/getFormats', () => ({
default: vi.fn(async () => ({
dateTime: {
short: {
day: 'numeric'
}
}
}))
}));

vi.mock('../../src/shared/NextIntlClientProvider', async () => ({
default: vi.fn(() => 'NextIntlClientProvider')
}));
Expand All @@ -18,20 +29,23 @@ it("doesn't read from headers if all relevant configuration is passed", async ()
children: null,
locale: 'en-GB',
now: new Date('2020-02-01T00:00:00.000Z'),
timeZone: 'Europe/London'
timeZone: 'Europe/London',
formats: {}
});

expect(result.type).toBe(NextIntlClientProvider);
expect(result.props).toEqual({
children: null,
locale: 'en-GB',
now: new Date('2020-02-01T00:00:00.000Z'),
timeZone: 'Europe/London'
timeZone: 'Europe/London',
formats: {}
});

expect(getLocale).not.toHaveBeenCalled();
expect(getNow).not.toHaveBeenCalled();
expect(getTimeZone).not.toHaveBeenCalled();
expect(getFormats).not.toHaveBeenCalled();
});

it('reads missing configuration from getter functions', async () => {
Expand All @@ -44,10 +58,18 @@ it('reads missing configuration from getter functions', async () => {
children: null,
locale: 'en-US',
now: new Date('2020-01-01T00:00:00.000Z'),
timeZone: 'America/New_York'
timeZone: 'America/New_York',
formats: {
dateTime: {
short: {
day: 'numeric'
}
}
}
});

expect(getLocale).toHaveBeenCalled();
expect(getNow).toHaveBeenCalled();
expect(getTimeZone).toHaveBeenCalled();
expect(getFormats).toHaveBeenCalled();
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, {ComponentProps} from 'react';
import getFormats from '../server/react-server/getFormats';
import {getLocale, getNow, getTimeZone} from '../server.react-server';
import BaseNextIntlClientProvider from '../shared/NextIntlClientProvider';

type Props = ComponentProps<typeof BaseNextIntlClientProvider>;

export default async function NextIntlClientProviderServer({
formats,
locale,
now,
timeZone,
Expand All @@ -14,6 +16,7 @@ export default async function NextIntlClientProviderServer({
<BaseNextIntlClientProvider
// We need to be careful about potentially reading from headers here.
// See https://github.com/amannn/next-intl/issues/631
formats={formats === undefined ? await getFormats() : formats}
locale={locale ?? (await getLocale())}
now={now ?? (await getNow())}
timeZone={timeZone ?? (await getTimeZone())}
Expand Down
10 changes: 10 additions & 0 deletions packages/next-intl/src/server/react-server/getFormats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {cache} from 'react';
import getConfig from './getConfig';

async function getFormatsCachedImpl() {
const config = await getConfig();
return config.formats;
}
const getFormatsCached = cache(getFormatsCachedImpl);

export default getFormatsCached;
2 changes: 1 addition & 1 deletion packages/next-intl/src/server/react-server/getLocale.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import getConfig from './getConfig';

async function getLocaleCachedImpl() {
const config = await getConfig();
return Promise.resolve(config.locale);
return config.locale;
}
const getLocaleCached = cache(getLocaleCachedImpl);

Expand Down

0 comments on commit 8b4c7c4

Please sign in to comment.