Skip to content

Commit 172656f

Browse files
amannnDuckThomfelix-quotez
authored
feat: next-intl@4 (#1412)
BREAKING CHANGE: See [announcement](https://next-intl.dev/blog/next-intl-4-0) --------- Co-authored-by: amannn <amannn@users.noreply.github.com> Co-authored-by: Thomas Wiringa <DuckThom@users.noreply.github.com> Co-authored-by: felix-quotez <felix@quotez.com>
1 parent d4d5b4a commit 172656f

File tree

325 files changed

+5322
-8036
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

325 files changed

+5322
-8036
lines changed

.github/workflows/main.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
jobs:
88
build:
99
name: Build, lint, and test
10-
runs-on: ubuntu-latest
10+
runs-on: macos-15
1111
env:
1212
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
1313
TURBO_TEAM: ${{ vars.TURBO_TEAM }}

.github/workflows/prerelease.yml .github/workflows/prerelease-canary.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: prerelease
1+
name: prerelease (canary)
22

33
on:
44
push:
@@ -26,7 +26,7 @@ jobs:
2626
- run: |
2727
sed -i 's/"use-intl": "workspace:\^"/"use-intl": "workspace:"/' ./packages/next-intl/package.json && git commit -am "use fixed version"
2828
- run: pnpm lerna publish 0.0.0-canary-${GITHUB_SHA::7} --no-git-reset --dist-tag canary --no-push --yes
29-
if: "${{startsWith(github.event.head_commit.message, 'fix: ') || startsWith(github.event.head_commit.message, 'feat: ')}}"
29+
if: "${{startsWith(github.event.head_commit.message, 'fix: ') || startsWith(github.event.head_commit.message, 'feat: ') || startsWith(github.event.head_commit.message, 'feat!: ')}}"
3030
env:
3131
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3232
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.github/workflows/release.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
git config --global user.name "${{ github.actor }}"
2525
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
2626
- run: pnpm run publish
27-
if: "${{startsWith(github.event.head_commit.message, 'fix: ') || startsWith(github.event.head_commit.message, 'feat: ')}}"
27+
if: "${{startsWith(github.event.head_commit.message, 'fix: ') || startsWith(github.event.head_commit.message, 'feat: ') || startsWith(github.event.head_commit.message, 'feat!: ')}}"
2828
env:
2929
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3030
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

CONTRIBUTORS.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ Note that the exclamation mark syntax (`!`) for indicating breaking changes is c
8686

8787
Other prefixes that are allowed and will _not_ create a release are the following:
8888

89-
1. `docs`: Documentation-only changes
89+
1. `docs`: Documentation-only changes and updated examples
9090
2. `test`: Missing tests were added or existing ones corrected
9191
3. `ci`: Changes to CI configuration files and scripts
9292
4. `build`: Changes that affect the build system or external dependencies

docs/src/components/Footer.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {useRouter} from 'next/router';
22
import config from '@/config';
33
import FooterLink from './FooterLink';
44
import FooterSeparator from './FooterSeparator';
5+
import FooterVersionSelector from './FooterVersionSelector';
56

67
export default function Footer() {
78
const router = useRouter();
@@ -19,6 +20,8 @@ export default function Footer() {
1920
<FooterLink href="/examples">Examples</FooterLink>
2021
<FooterSeparator />
2122
<FooterLink href="/blog">Blog</FooterLink>
23+
<FooterSeparator />
24+
<FooterVersionSelector />
2225
</div>
2326
<div>
2427
<FooterLink href={config.blueskyUrl} target="_blank">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {ChangeEvent} from 'react';
2+
3+
export default function FooterVersionSelector() {
4+
function onChange(event: ChangeEvent<HTMLSelectElement>) {
5+
const version = event.target.value;
6+
window.location.href = `https://${version}.next-intl.dev`;
7+
}
8+
9+
return (
10+
<select
11+
className="inline-flex appearance-none items-center bg-transparent py-3 text-xs text-slate-500 transition-colors hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
12+
defaultValue="v4"
13+
onChange={onChange}
14+
>
15+
<option value="v3">v3</option>
16+
<option value="v4">v4</option>
17+
</select>
18+
);
19+
}

docs/src/pages/blog/_meta.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export default {
33
title: 'Overview'
44
},
55
'next-intl-4-0': {
6-
title: 'next-intl 4.0 beta',
6+
title: 'next-intl 4.0',
77
display: 'hidden'
88
},
99
'next-intl-3-22': {

docs/src/pages/blog/index.mdx

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import StayUpdated from '@/components/StayUpdated.mdx';
66
<div className="flex flex-col gap-4 py-8">
77
<BlogPostLink
88
href="/blog/next-intl-4-0"
9-
title="next-intl 4.0 beta"
10-
date="Dec 23, 2024"
9+
title="next-intl 4.0"
10+
date="Mar 12, 2025"
1111
author="By Jan Amann"
1212
/>
1313
<BlogPostLink

docs/src/pages/blog/next-intl-4-0.mdx

+14-14
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
---
2-
title: next-intl 4.0 beta
2+
title: next-intl 4.0
33
---
44

55
import PartnerContentLink from '@/components/PartnerContentLink';
66
import StayUpdated from '@/components/StayUpdated.mdx';
77

8-
# next-intl 4.0 beta
8+
# next-intl 4.0
99

10-
<small>Dec 23, 2024 · by Jan Amann</small>
10+
<small>Mar 12, 2025 · by Jan Amann</small>
1111

1212
After a year of feature development, this release focuses on streamlining the API surface while maintaining the core architecture of `next-intl`. With many improvements already released in [previous minor versions](/blog/next-intl-3-22), this update introduces several enhancements that will improve your development experience and make working with internationalization even more seamless.
1313

@@ -44,7 +44,7 @@ declare module 'next-intl' {
4444
}
4545
```
4646

47-
See the updated [TypeScript augmentation](https://v4.next-intl.dev/docs/workflows/typescript) guide.
47+
See the updated [TypeScript augmentation](/docs/workflows/typescript) guide.
4848

4949
## Strictly-typed locale
5050

@@ -65,7 +65,7 @@ declare module 'next-intl' {
6565

6666
By doing so, APIs like `useLocale()` or `<Link />` that either return or receive a `locale` will now pick up your app-specific `Locale` type, improving type safety across your app.
6767

68-
To simplify narrowing of `string`-based locales, a `hasLocale` function has been added. This can for example be used in [`i18n/request.ts`](https://v4.next-intl.dev/docs/getting-started/app-router/with-i18n-routing#i18n-request) to return a valid locale:
68+
To simplify narrowing of `string`-based locales, a `hasLocale` function has been added. This can for example be used in [`i18n/request.ts`](/docs/getting-started/app-router/with-i18n-routing#i18n-request) to return a valid locale:
6969

7070
```tsx
7171
import {getRequestConfig} from 'next-intl/server';
@@ -148,13 +148,13 @@ t('followers', {count: 30000});
148148
"{count, number} followers"
149149
```
150150

151-
Due to a current limitation in TypeScript, this feature is opt-in for now. Please refer to the [strict arguments](https://v4.next-intl.dev/docs/workflows/typescript#messages-arguments) docs to learn how to enable it.
151+
Due to a current limitation in TypeScript, this feature is opt-in for now. Please refer to the [strict arguments](/docs/workflows/typescript#messages-arguments) docs to learn how to enable it.
152152

153153
## GDPR compliance [#gdpr-compliance]
154154

155155
In order to comply with the current GDPR regulations, the following changes have been made and are relevant to you if you're using the `next-intl` middleware for i18n routing:
156156

157-
1. The locale cookie now defaults to a session cookie that expires when a browser is closed.
157+
1. The locale cookie now defaults to a session cookie that expires when the browser is closed.
158158
2. The locale cookie is now only set when a user switches to a locale that doesn't match the `accept-language` header.
159159

160160
If you want to increase the cookie expiration, e.g. because you're informing users about the usage of cookies or if GDPR doesn't apply to your app, you can use the `maxAge` attribute to do so:
@@ -174,11 +174,11 @@ export const routing = defineRouting({
174174
});
175175
```
176176

177-
Since the cookie is now only available after a locale switch, make sure to not rely on it always being present. E.g. if you need access to the user's locale in a [Route Handler](https://v4.next-intl.dev/docs/environments/actions-metadata-route-handlers#route-handlers), a reliable option is to provide the locale as a search param (e.g. `/api/posts/12?locale=en`).
177+
Since the cookie is now only available after a locale switch, make sure to not rely on it always being present. E.g. if you need access to the user's locale in a [Route Handler](/docs/environments/actions-metadata-route-handlers#route-handlers), a reliable option is to provide the locale as a search param (e.g. `/api/posts/12?locale=en`).
178178

179-
As part of this change, disabling a cookie now requires you to set [`localeCookie: false`](https://v4.next-intl.dev/docs/routing#locale-cookie) in your routing configuration. Previously, `localeDetection: false` ambiguously also disabled the cookie from being set, but since a separate `localeCookie` option was introduced recently, this should now be used instead.
179+
As part of this change, disabling a cookie now requires you to set [`localeCookie: false`](/docs/routing#locale-cookie) in your routing configuration. Previously, `localeDetection: false` ambiguously also disabled the cookie from being set, but since a separate `localeCookie` option was introduced recently, this should now be used instead.
180180

181-
Learn more in the [locale cookie](https://v4.next-intl.dev/docs/routing#locale-cookie) docs.
181+
Learn more in the [locale cookie](/docs/routing#locale-cookie) docs.
182182

183183
## Modernized build output
184184

@@ -266,7 +266,7 @@ This will create the following structure:
266266
- `example.no`: `no-NO`
267267
- `example.no/en`: `en-NO`
268268

269-
Learn more in the updated docs for [`domains`](https://v4.next-intl.dev/docs/routing#domains).
269+
Learn more in the updated docs for [`domains`](/docs/routing#domains).
270270

271271
## Preparation for upcoming Next.js features [#nextjs-future]
272272

@@ -300,11 +300,9 @@ For a smooth upgrade, please initially upgrade to the latest v3.x version and ch
300300
Afterwards, you can upgrade by running:
301301

302302
```
303-
npm install next-intl@v4-beta
303+
npm install next-intl@4
304304
```
305305

306-
The beta docs are available here: [v4.next-intl.dev](https://v4.next-intl.dev)
307-
308306
I'd love to hear about your experiences with `next-intl@4.0`! Join the conversation in the [discussions](https://github.com/amannn/next-intl/discussions/1631).
309307

310308
## Thank you!
@@ -315,6 +313,8 @@ A special thank you goes to <PartnerContentLink href="https://crowdin.com/">Crow
315313

316314
—Jan
317315

316+
(this post has been updated from an initial announcement for the 3.0 release candidate)
317+
318318
PS: Have you heard that [learn.next-intl.dev](https://learn.next-intl.dev) is coming?
319319

320320
<StayUpdated />

docs/src/pages/docs/environments/actions-metadata-route-handlers.mdx

+8-2
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,9 @@ Note that by default, `next-intl` returns [the `link` response header](/docs/rou
173173

174174
Next.js supports providing alternate URLs per language via the [`alternates` entry](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap#generate-a-localized-sitemap). You can construct a list of entries for each pathname and locale as follows:
175175

176-
```tsx filename="app/sitemap.ts" {4-5,8-9}
176+
```tsx filename="app/sitemap.ts" {5-6,9-10}
177177
import {MetadataRoute} from 'next';
178+
import {Locale} from 'next-intl';
178179
import {routing, getPathname} from '@/i18n/routing';
179180

180181
// Adapt this as necessary
@@ -198,7 +199,7 @@ function getEntries(href: Href) {
198199
}));
199200
}
200201

201-
function getUrl(href: Href, locale: (typeof routing.locales)[number]) {
202+
function getUrl(href: Href, locale: Locale) {
202203
const pathname = getPathname({locale, href});
203204
return host + pathname;
204205
}
@@ -230,12 +231,17 @@ You can use `next-intl` in [Route Handlers](https://nextjs.org/docs/app/building
230231

231232
```tsx filename="app/api/hello/route.tsx"
232233
import {NextResponse} from 'next/server';
234+
import {hasLocale} from 'next-intl';
233235
import {getTranslations} from 'next-intl/server';
236+
import {routing} from '@/i18n/routing';
234237

235238
export async function GET(request) {
236239
// Example: Receive the `locale` via a search param
237240
const {searchParams} = new URL(request.url);
238241
const locale = searchParams.get('locale');
242+
if (!hasLocale(routing.locales, locale)) {
243+
return NextResponse.json({error: 'Invalid locale'}, {status: 400});
244+
}
239245

240246
const t = await getTranslations({locale, namespace: 'Hello'});
241247
return NextResponse.json({title: t('title')});

docs/src/pages/docs/environments/error-files.mdx

+5-5
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,16 @@ export default function RootLayout({children}) {
7777
}
7878
```
7979

80-
For the 404 page to render, we need to call the `notFound` function in the root layout when we detect an incoming `locale` param that isn't a valid locale.
80+
For the 404 page to render, we need to call the `notFound` function in the root layout when we detect an incoming `locale` param that isn't valid.
8181

8282
```tsx filename="app/[locale]/layout.tsx"
83+
import {hasLocale} from 'next-intl';
8384
import {notFound} from 'next/navigation';
85+
import {routing} from '@/i18n/routing';
8486

85-
export default async function LocaleLayout({children, params}) {
87+
export default function LocaleLayout({children, params}) {
8688
const {locale} = await params;
87-
88-
// Ensure that the incoming `locale` is valid
89-
if (!routing.locales.includes(locale as any)) {
89+
if (!hasLocale(routing.locales, locale)) {
9090
notFound();
9191
}
9292

docs/src/pages/docs/environments/server-client-components.mdx

+10-14
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,17 @@ These functions are available:
6969

7070
Components that aren't declared with the `async` keyword and don't use interactive features like `useState`, are referred to as [shared components](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#sharing-code-between-server-and-client). These can render either as a Server or Client Component, depending on where they are imported from.
7171

72-
In Next.js, Server Components are the default, and therefore shared components will typically execute as Server Components.
72+
In Next.js, Server Components are the default, and therefore shared components will typically execute as Server Components:
7373

7474
```tsx filename="UserDetails.tsx"
7575
import {useTranslations} from 'next-intl';
7676

7777
export default function UserDetails({user}) {
7878
const t = useTranslations('UserProfile');
7979

80+
// This component will execute as a Server Component by default.
81+
// However, if it is imported from a Client Component, it will
82+
// execute as a Client Component.
8083
return (
8184
<section>
8285
<h2>{t('title')}</h2>
@@ -112,11 +115,13 @@ Regarding performance, async functions and hooks can be used interchangeably. Th
112115

113116
## Using internationalization in Client Components
114117

115-
Depending on your situation, you may need to handle internationalization in Client Components. While providing all messages to the client side is typically the easiest way to [get started](/docs/getting-started/app-router#layout) and a reasonable approach for many apps, you can be more selective about which messages are passed to the client side if you're interested in optimizing the performance of your app.
118+
Depending on your situation, you may need to handle internationalization in Client Components. Providing all messages to the client side is the easiest way to get started, therefore `next-intl` automatically does this when you render [`NextIntlClientProvider`](/docs/usage/configuration#nextintlclientprovider). This is a reasonable approach for many apps.
119+
120+
However, you can be more selective about which messages are passed to the client side if you're interested in optimizing the performance of your app.
116121

117122
There are several options for using translations from `next-intl` in Client Components, listed here in order of enabling the best performance:
118123

119-
### Option 1: Passing translations to Client Components
124+
### Option 1: Passing translated labels to Client Components
120125

121126
The preferred approach is to pass the processed labels as props or `children` from a Server Component.
122127

@@ -275,8 +280,6 @@ In particular, page and search params are often a great option because they offe
275280

276281
### Option 3: Providing individual messages
277282

278-
To reduce bundle size, `next-intl` doesn't automatically provide [messages](/docs/usage/configuration#messages) or [formats](/docs/usage/configuration#formats) to Client Components.
279-
280283
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.
281284

282285
```tsx filename="Counter.tsx"
@@ -312,22 +315,16 @@ An automatic, compiler-driven approach is being evaluated in [`next-intl#1`](htt
312315

313316
### Option 4: Providing all messages
314317

315-
If you're building a highly dynamic app where most components use React's interactive features, you may prefer to make all messages available to Client Components.
318+
If you're building a highly dynamic app where most components use React's interactive features, you may prefer to make all messages available to Client Components—this is the default behavior of `next-intl`.
316319

317320
```tsx filename="layout.tsx" /NextIntlClientProvider/
318321
import {NextIntlClientProvider} from 'next-intl';
319-
import {getMessages} from 'next-intl/server';
320322

321323
export default async function RootLayout(/* ... */) {
322-
// Receive messages provided in `i18n/request.ts`
323-
const messages = await getMessages();
324-
325324
return (
326325
<html lang={locale}>
327326
<body>
328-
<NextIntlClientProvider messages={messages}>
329-
{children}
330-
</NextIntlClientProvider>
327+
<NextIntlClientProvider>{children}</NextIntlClientProvider>
331328
</body>
332329
</html>
333330
);
@@ -366,7 +363,6 @@ The component accepts the following props that are not serializable:
366363

367364
1. [`onError`](/docs/usage/configuration#error-handling)
368365
2. [`getMessageFallback`](/docs/usage/configuration#error-handling)
369-
3. Rich text elements for [`defaultTranslationValues`](/docs/usage/configuration#default-translation-values)
370366

371367
To configure these, you can wrap `NextIntlClientProvider` with another component that is marked with `'use client'` and defines the relevant props.
372368

0 commit comments

Comments
 (0)