Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: next-intl@4 #1412

Draft
wants to merge 71 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
a8fb0e3
fix: Don't commit canary releases
amannn Jun 20, 2024
cf3f8b0
Improve docs [skip ci]
amannn Jun 20, 2024
4fd9b20
fix: Try out updated credits
amannn Jun 20, 2024
47239f9
v3.15.3-canary.0
amannn Jun 20, 2024
0d60a72
Revert "v3.15.3-canary.0"
amannn Jun 20, 2024
c459669
Merge remote-tracking branch 'origin/main' into canary
amannn Jun 20, 2024
8d3ab38
fix: Prefer more specific routes in `usePathname` when detecting the …
amannn Jun 26, 2024
605c9a6
Merge remote-tracking branch 'origin/main' into canary
amannn Jun 26, 2024
cb15046
Merge branch 'fix/sort-pathnames-usepathname' into canary
amannn Jun 26, 2024
11262b8
Merge remote-tracking branch 'origin/main' into canary
amannn Jul 11, 2024
7976376
feat: Support `trailingSlash: true` in Next.js config (#1188)
amannn Jul 11, 2024
b352c10
fix: Release please?
amannn Jul 11, 2024
cba448a
Merge remote-tracking branch 'origin/main' into canary
amannn Jul 11, 2024
0c670b2
Merge remote-tracking branch 'origin/main' into canary
amannn Jul 12, 2024
5dc7f53
Merge remote-tracking branch 'origin/main' into canary
amannn Jul 12, 2024
11dcbcc
Merge remote-tracking branch 'origin/main' into canary
amannn Jul 12, 2024
1b074da
Merge remote-tracking branch 'origin/main' into canary
amannn Jul 12, 2024
e98682b
Merge remote-tracking branch 'origin/main' into canary
amannn Jul 15, 2024
7c6f533
Merge branch 'main' into canary
amannn Aug 23, 2024
8b4c7c4
feat!: Automatically inherit `formats` when `NextIntlClientProvider` …
amannn Aug 26, 2024
d80d691
feat!: Inherit context between providers (#1413)
amannn Oct 9, 2024
8158de4
Merge remote-tracking branch 'origin/canary' into HEAD
amannn Oct 9, 2024
7dd54c1
Merge remote-tracking branch 'origin/canary' into v4
amannn Oct 24, 2024
ccaef97
fix lint
amannn Oct 24, 2024
25a0dc8
bump size
amannn Oct 24, 2024
9f4754c
feat!: Make package ESM-only, target modern browsers, use modern JSX …
amannn Oct 29, 2024
5b5f0e3
feat!: Remove deprecated APIs (#1479)
amannn Oct 29, 2024
eb5bf09
feat!: Bump minimum required `typescript` version to 5 for projects u…
amannn Oct 29, 2024
a6238f4
feat!: Remove deprecated APIs pt. 2 (#1482)
amannn Oct 29, 2024
829998e
feat!: Infer default `locale` for `NextIntlClientProvider` from `useP…
amannn Oct 29, 2024
a9c35db
add comment
amannn Oct 29, 2024
5402851
feat!: Require `locale` to be returned from `getRequestConfig` (#1486)
amannn Oct 29, 2024
b4e4d1d
feat!: Decrease cookie expiration to 5 hours, only set cookie when ne…
amannn Oct 30, 2024
d83abc2
feat: Return type-safe `IntlMessages` from `useMessages` & `getMessag…
amannn Oct 30, 2024
a7aaf56
feat!: Revamp augmented types and add support for typed `Locale` (#1495)
amannn Nov 1, 2024
2388c9b
remove unused file
amannn Nov 1, 2024
01268f6
feat: Type-safe ICU arguments (#1499)
amannn Nov 7, 2024
0bb75c5
decrease size by making plain message error handling dev-only
amannn Nov 7, 2024
dd28ac1
fix: Allow to merge inline formats with global formats in `t` (#1523)
amannn Nov 7, 2024
4675f7e
Merge remote-tracking branch 'origin/main' into v4
amannn Nov 8, 2024
c154409
move/rename file
amannn Nov 8, 2024
887034d
simplify signature
amannn Nov 8, 2024
ab22a92
temporarily disable exports that cause OOM
amannn Nov 8, 2024
b9c515e
simplify types for `createTranslator`
amannn Nov 8, 2024
8ac7206
fix: Fix recursion bug in typed ICU args (#1527)
amannn Nov 8, 2024
21f2dac
remove outdated constant
amannn Nov 12, 2024
2a92f17
feat!: Remove default of `now={new Date()}` from `NextIntlClientProvi…
amannn Nov 14, 2024
dc36097
feat!: Don't read a default for `useLocale` from `useParams.locale` o…
amannn Nov 14, 2024
3ff1923
decrease size, relax next.js peer dependency
amannn Nov 14, 2024
f44ae15
feat: Adopt shared ICU type parser (#1549)
amannn Nov 14, 2024
7d89d54
chore: Strict type imports with ESLint (#1524)
amannn Nov 14, 2024
c2a8984
Merge remote-tracking branch 'origin/main' into v4
amannn Nov 15, 2024
00a79d4
feat: publish v4 prerelease
amannn Nov 15, 2024
ba18078
rename canary workflow
amannn Nov 15, 2024
6f1a39d
unique workflow names [skip ci]
amannn Nov 15, 2024
486a7bd
docs: minor fixes
amannn Nov 15, 2024
dfa5ea0
docs: blog post draft
amannn Nov 18, 2024
9ea117c
fix: Disallow string dates, improve autocomplete (#1557)
amannn Nov 18, 2024
c8eb4ad
fix: Use `fs.watch` instead of `chokidar`
amannn Nov 19, 2024
5a7f7be
fix: Move locale validation to `defineRouting` (#1560)
amannn Nov 19, 2024
bfcd251
docs: Proofread blog post
amannn Nov 19, 2024
7b755e9
fix: update warning for now fallback in `createFormatter`
amannn Nov 19, 2024
18156c4
fix: use v4-beta tag instead
amannn Nov 19, 2024
0b2c951
docs: reference new tag
amannn Nov 19, 2024
6275030
feat!: Disallow passing `null`, `undefined` or `boolean` as an ICU ar…
amannn Nov 20, 2024
c323050
fix: release for "feat!: " as well
amannn Nov 20, 2024
8cce53e
docs: performance notes of messages augmentation
amannn Nov 20, 2024
6d94a9e
docs: fix toc, note in gdpr release notes [skip ci]
amannn Nov 28, 2024
db95243
docs: Update legacy example to use `next-intl` instead of `use-intl` …
amannn Nov 28, 2024
21b882a
docs: improve relative time formatting docs (also fix release conditi…
amannn Nov 28, 2024
cb6b998
Merge remote-tracking branch 'origin/main' into v4
amannn Nov 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
jobs:
build:
name: Build, lint, and test
runs-on: ubuntu-latest
runs-on: macos-15
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: prerelease
name: prerelease (canary)

on:
push:
Expand Down Expand Up @@ -26,7 +26,7 @@ jobs:
- run: |
sed -i 's/"use-intl": "workspace:\^"/"use-intl": "workspace:"/' ./packages/next-intl/package.json && git commit -am "use fixed version"
- run: pnpm lerna publish 0.0.0-canary-${GITHUB_SHA::7} --no-git-reset --dist-tag canary --no-push --yes
if: "${{startsWith(github.event.head_commit.message, 'fix: ') || startsWith(github.event.head_commit.message, 'feat: ')}}"
if: "${{startsWith(github.event.head_commit.message, 'fix: ') || startsWith(github.event.head_commit.message, 'feat: ') || startsWith(github.event.head_commit.message, 'feat!: ')}}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/prerelease-v4.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: prerelease (v4)

on:
push:
branches:
- v4

jobs:
main:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
registry-url: 'https://registry.npmjs.org'
node-version: 20.x
cache: 'pnpm'
- run: pnpm install
- run: |
git config --global user.name "${{ github.actor }}"
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
- run: |
sed -i 's/"use-intl": "workspace:\^"/"use-intl": "workspace:"/' ./packages/next-intl/package.json && git commit -am "use fixed version"
- run: pnpm lerna publish 4.0.0-beta-${GITHUB_SHA::7} --no-git-reset --dist-tag v4-beta --no-push --yes
if: "${{startsWith(github.event.head_commit.message, 'fix: ') || startsWith(github.event.head_commit.message, 'feat: ') || startsWith(github.event.head_commit.message, 'feat!: ')}}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
git config --global user.name "${{ github.actor }}"
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
- run: pnpm run publish
if: "${{startsWith(github.event.head_commit.message, 'fix: ') || startsWith(github.event.head_commit.message, 'feat: ')}}"
if: "${{startsWith(github.event.head_commit.message, 'fix: ') || startsWith(github.event.head_commit.message, 'feat: ') || startsWith(github.event.head_commit.message, 'feat!: ')}}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Expand Down
4 changes: 4 additions & 0 deletions docs/src/pages/blog/_meta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ export default {
index: {
title: 'Overview'
},
'next-intl-4-0': {
title: 'next-intl 4.0',
display: 'hidden'
},
'next-intl-3-22': {
title: 'next-intl 3.22',
display: 'hidden'
Expand Down
6 changes: 6 additions & 0 deletions docs/src/pages/blog/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import StayUpdated from '@/components/StayUpdated.mdx';
# next-intl blog

<div className="flex flex-col gap-4 py-8">
<BlogPostLink
href="/blog/next-intl-4-0"
title="next-intl 4.0"
date="Dec XX, 2024"
author="By Jan Amann"
/>
<BlogPostLink
href="/blog/next-intl-3-22"
title="next-intl 3.22: Incrementally moving forward"
Expand Down
249 changes: 249 additions & 0 deletions docs/src/pages/blog/next-intl-4-0.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
---
title: next-intl 4.0
---

import PartnerContentLink from '@/components/PartnerContentLink';
import StayUpdated from '@/components/StayUpdated.mdx';

# next-intl 4.0

<small>Dec XX, 2024 · by Jan Amann</small>

(this post is still a draft)

After a year of feature development, this release mostly aims to clean up the API surface to ensure `next-intl` remains lean—there should be no big surprises. Many significant improvements have already been shipped in [minor versions](/blog/next-intl-3-22) previously. However, this release also comes with a series of improvements that you might find useful.

Here's what's new in `next-intl@4.0`:

1. [**Revamped augmented types**](#revamped-augmented-types)
2. [**Strictly-typed locale**](#strictly-typed-locale)
3. [**Strictly-typed ICU arguments**](#strictly-typed-icu-arguments)
4. [**GDPR compliance**](#gdpr-compliance)
5. [**Modernized build output**](#modernized-build-output)
6. [**Preparation for upcoming Next.js features**](#nextjs-future)

Please also have a look at the [other breaking changes](#other-breaking-changes) before you [upgrade](#upgrade-now).

## Revamped augmented types

After type-safe [`Formats`](/docs/usage/configuration#formats) was added in `next-intl@3.20`, it became clear that a new API was needed that centralizes the registration of augmented types.

With `next-intl@4.0`, both `Messages` as well as `Formats` can now be registered under a single type that is scoped to `next-intl` and no longer affects the global scope:

```tsx
// global.d.ts

import {formats} from '@/i18n/request';
import en from './messages/en.json';

declare module 'next-intl' {
interface AppConfig {
Formats: typeof formats;
Messages: typeof en;
}
}
```

See the updated [TypeScript augmentation](/docs/workflows/typescript) guide.

## Strictly-typed locale

Building on the new type augmentation mechanism, `next-intl@4.0` now allows you to strictly type locales across your app:

```tsx
// global.d.ts

import {routing} from '@/i18n/routing';

declare module 'next-intl' {
interface AppConfig {
// ...
Locale: (typeof routing.locales)[number];
}
}
```

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.

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:

```tsx
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';

export default getRequestConfig(async ({requestLocale}) => {
// Typically corresponds to the `[locale]` segment
const requested = await requestLocale;
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale;

return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
});
```

Furthermore, the `Locale` type can be imported into your app code in case you're passing a locale to another function and want to ensure type safety:

```tsx
import {Locale} from 'next-intl';

async function getPosts(locale: Locale) {
// ...
}
```

Note that strictly-typing the `Locale` is optional and can be used as desired in case you wish to have additional guardrails in your app.

## Strictly-typed ICU arguments

How type-safe can your app be?

The quest to bring type safety to the last corner of `next-intl` has led me down a rabbit hole with the discovery of an ICU parser by [Marco Schumacher](https://github.com/schummar)—written entirely in types. Marco kindly published his implementation for usage in `next-intl`, with me only adding support for rich tags on top.

Check it out:

```tsx
// "Hello {name}"
t('message');
// ^? Expected 2 arguments

// "Hello {name}"
t('message', {});
// ^? {name: string}

// "It's {today, date, long}"
t('message', {});
// ^? {today: Date}

// "Page {page, number} out of {total, number}"
t('message', {});
// ^? {page: number, total: number}

// "You have {count, plural, =0 {no followers yet} =1 {one follower} other {# followers}}."
t('message', {});
// ^? {count: number}

// "Country: {country, select, US {United States} CA {Canada} other {Other}}"
t('message', {});
// ^? {country: 'US' | 'CA' | (string & {})}

// "Please refer to the <link>guidelines</link>."
t('message', {});
// ^? {link: (chunks: ReactNode) => ReactNode}
```

With this type inference in place, you can now use autocompletion in your IDE to get suggestions for the available arguments of a given ICU message and catch potential errors early.

This also addresses one of my favorite pet peeves:

```tsx
t('followers', {count: 30000});
```

```json
// ✖️ Would be: "30000 followers"
"{count} followers"

// ✅ Valid: "30,000 followers"
"{count, number} followers"
```

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.

## GDPR compliance [#gdpr-compliance]

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

1. The locale cookie expiration has been decreased to 5 hours.
2. The locale cookie is now only set when a user switches to a locale that doesn't match the `accept-language` header.

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:

```tsx
// i18n/routing.tsx

import {defineRouting} from 'next-intl/routing';

export const routing = defineRouting({
// ...

localeCookie: {
// Expire in one year
maxAge: 60 * 60 * 24 * 365
}
});
```

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`).

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.

Learn more in the [locale cookie](/docs/routing#locale-cookie) docs.

## Modernized build output

The build output of `next-intl` has been modernized and now leverages the following optimizations:

1. **ESM-only:** To enable enhanced tree-shaking and align with the modern JavaScript ecosystem, `next-intl` is now ESM-only. The only exception is `next-intl/plugin` which is published both as CommonJS as well as ESM, due to `next.config.js` still being popular.
2. **Modern JSX transform:** The peer dependency for React has been bumped to v17 in order to use the more efficient, modern JSX transform.
3. **Modern syntax:** Syntax is now compiled down to the Browserslist `defaults` query, which is a shortcut for ">0.5%, last 2 versions, Firefox ESR, not dead"—a baseline that is considered a reasonable target for modern apps.

With these changes, the bundle size of `next-intl` has been reduced by ~7% ([all details](https://github.com/amannn/next-intl/pull/1470)).

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

To ensure that the sails of `next-intl` are set for a steady course in the upcoming future, I've investigated the implications of upcoming Next.js features like [Partial Prerendering](https://nextjs.org/docs/app/api-reference/next-config-js/ppr) and [`dynamicIO`](https://nextjs.org/docs/canary/app/api-reference/config/next-config-js/dynamicIO) for `next-intl`.

This led to two minor changes:

1. If you don't already have a `NextIntlClientProvider` in your app that wraps all Client Components, you now have to add one (see [PR #1541](https://github.com/amannn/next-intl/pull/1541) for details).
2. If you're using `format.relativeTime` in Client Components, you may need to provide the `now` argument explicitly now (see [PR #1536](https://github.com/amannn/next-intl/pull/1536) for details).

While the mentioned Next.js features are still under development and may change, these two changes seem reasonable to me in any case—and ideally will be all that's necessary to adapt for `next-intl` to get the most out of these upcoming capabilities.

As a closing note for this section, it seems like another feature is on its way to Next.js: [`rootParams`](https://github.com/vercel/next.js/pull/72837).

```tsx
import {unstable_rootParams as rootParams} from 'next/server';

async function Component() {
// The ability to read params deeply in
// Server Components ... finally!
const {locale} = await rootParams();
}
```

If things go well, I think this will finally fill in the [missing piece](https://github.com/vercel/next.js/discussions/58862) that enables apps with i18n routing to support static rendering without workarounds like `setRequestLocale`. I hope to have more to share on this soon!

## Other breaking changes

1. Return type-safe messages from `useMessages` and `getMessages` (see [PR #1489](https://github.com/amannn/next-intl/pull/1489))
2. Inherit context in case nested `NextIntlClientProvider` instances are present (see [PR #1413](https://github.com/amannn/next-intl/pull/1413))
3. Automatically inherit formats when `NextIntlClientProvider` is rendered from a Server Component (see [PR #1191](https://github.com/amannn/next-intl/pull/1191))
4. Require locale to be returned from `getRequestConfig` (see [PR #1486](https://github.com/amannn/next-intl/pull/1486))
5. Disallow passing `null`, `undefined` or `boolean` as an ICU argument (see [PR #1561](https://github.com/amannn/next-intl/pull/1561))
6. Bump minimum required typescript version to 5 for projects using TypeScript (see [PR #1481](https://github.com/amannn/next-intl/pull/1481))
7. Remove deprecated APIs (see [PR #1479](https://github.com/amannn/next-intl/pull/1479))
8. Remove deprecated APIs pt. 2 (see [PR #1482](https://github.com/amannn/next-intl/pull/1482))

## Upgrade now

For a smooth upgrade, please initially upgrade to the latest v3.x version and check for deprecation warnings.

Afterwards, you can upgrade by running:

```
npm install next-intl@v4-beta
```

## Thank you

I want to sincerely thank everyone who has helped to make `next-intl` what it is today. A special thank you goes to <PartnerContentLink href="https://crowdin.com/">Crowdin</PartnerContentLink>, the primary sponsor of `next-intl`, enabling me to regularly work on this project.

—Jan

<StayUpdated />
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,9 @@ Note that by default, `next-intl` returns [the `link` response header](/docs/rou

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) as of version 14.2. You can use your default locale for the main URL and provide alternate URLs based on all locales that your app supports. Keep in mind that also the default locale should be included in the `alternates` object.

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

// Adapt this as necessary
Expand All @@ -179,7 +180,7 @@ function getEntry(href: Href) {
};
}

function getUrl(href: Href, locale: (typeof routing.locales)[number]) {
function getUrl(href: Href, locale: Locale) {
const pathname = getPathname({locale, href});
return host + pathname;
}
Expand All @@ -206,12 +207,16 @@ You can use `next-intl` in [Route Handlers](https://nextjs.org/docs/app/building

```tsx filename="app/api/hello/route.tsx"
import {NextResponse} from 'next/server';
import {hasLocale} from 'next-intl';
import {getTranslations} from 'next-intl/server';

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

const t = await getTranslations({locale, namespace: 'Hello'});
return NextResponse.json({title: t('title')});
Expand Down
7 changes: 4 additions & 3 deletions docs/src/pages/docs/environments/error-files.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,15 @@ export default function RootLayout({children}) {
}
```

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.
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.

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

export default function LocaleLayout({children, params: {locale}}) {
// Ensure that the incoming `locale` is valid
if (!routing.locales.includes(locale as any)) {
if (!hasLocale(routing.locales, locale)) {
notFound();
}

Expand Down
Loading