From d1a0308e72d7f5111a7dcc6f34efd22d8ad0e576 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Mon, 11 Nov 2024 11:47:52 +0100 Subject: [PATCH 01/28] Use `dynamicParams = false` instead of manually calling `notFound()` --- .../example-app-router/src/app/[locale]/layout.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/example-app-router/src/app/[locale]/layout.tsx b/examples/example-app-router/src/app/[locale]/layout.tsx index 7dc3091ae..5798cdf87 100644 --- a/examples/example-app-router/src/app/[locale]/layout.tsx +++ b/examples/example-app-router/src/app/[locale]/layout.tsx @@ -1,5 +1,4 @@ -import {notFound} from 'next/navigation'; -import {Locale, hasLocale} from 'next-intl'; +import {Locale} from 'next-intl'; import {getTranslations, setRequestLocale} from 'next-intl/server'; import {ReactNode} from 'react'; import BaseLayout from '@/components/BaseLayout'; @@ -24,14 +23,13 @@ export async function generateMetadata({ }; } +// Return a 404 for unknown locales +export const dynamicParams = false; + export default async function LocaleLayout({ children, params: {locale} }: Props) { - if (!hasLocale(routing.locales, locale)) { - notFound(); - } - // Enable static rendering setRequestLocale(locale); From f54b01d412111bdcce0e317eb3e9a1ee9fe211c8 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Mon, 11 Nov 2024 12:25:23 +0100 Subject: [PATCH 02/28] upgrade example-app-router to latest next.js canary --- examples/example-app-router/next-env.d.ts | 2 +- examples/example-app-router/package.json | 2 +- .../src/app/[locale]/layout.tsx | 13 +- .../src/app/[locale]/page.tsx | 6 +- .../src/app/[locale]/pathnames/page.tsx | 6 +- pnpm-lock.yaml | 196 ++++++++++++++---- 6 files changed, 175 insertions(+), 50 deletions(-) diff --git a/examples/example-app-router/next-env.d.ts b/examples/example-app-router/next-env.d.ts index 40c3d6809..1b3be0840 100644 --- a/examples/example-app-router/next-env.d.ts +++ b/examples/example-app-router/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/examples/example-app-router/package.json b/examples/example-app-router/package.json index c6eeb30e1..20293aa44 100644 --- a/examples/example-app-router/package.json +++ b/examples/example-app-router/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "clsx": "^2.1.1", - "next": "^14.2.4", + "next": "15.0.4-canary.5", "next-intl": "^3.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/examples/example-app-router/src/app/[locale]/layout.tsx b/examples/example-app-router/src/app/[locale]/layout.tsx index 5798cdf87..02ada88c6 100644 --- a/examples/example-app-router/src/app/[locale]/layout.tsx +++ b/examples/example-app-router/src/app/[locale]/layout.tsx @@ -6,16 +6,15 @@ import {routing} from '@/i18n/routing'; type Props = { children: ReactNode; - params: {locale: Locale}; + params: Promise<{locale: Locale}>; }; export function generateStaticParams() { return routing.locales.map((locale) => ({locale})); } -export async function generateMetadata({ - params: {locale} -}: Omit) { +export async function generateMetadata({params}: Omit) { + const {locale} = await params; const t = await getTranslations({locale, namespace: 'LocaleLayout'}); return { @@ -26,11 +25,9 @@ export async function generateMetadata({ // Return a 404 for unknown locales export const dynamicParams = false; -export default async function LocaleLayout({ - children, - params: {locale} -}: Props) { +export default async function LocaleLayout({children, params}: Props) { // Enable static rendering + const {locale} = await params; setRequestLocale(locale); return {children}; diff --git a/examples/example-app-router/src/app/[locale]/page.tsx b/examples/example-app-router/src/app/[locale]/page.tsx index ea963c340..8f2dc3165 100644 --- a/examples/example-app-router/src/app/[locale]/page.tsx +++ b/examples/example-app-router/src/app/[locale]/page.tsx @@ -1,13 +1,15 @@ import {Locale, useTranslations} from 'next-intl'; import {setRequestLocale} from 'next-intl/server'; +import {use} from 'react'; import PageLayout from '@/components/PageLayout'; type Props = { - params: {locale: Locale}; + params: Promise<{locale: Locale}>; }; -export default function IndexPage({params: {locale}}: Props) { +export default function IndexPage({params}: Props) { // Enable static rendering + const {locale} = use(params); setRequestLocale(locale); const t = useTranslations('IndexPage'); diff --git a/examples/example-app-router/src/app/[locale]/pathnames/page.tsx b/examples/example-app-router/src/app/[locale]/pathnames/page.tsx index b53ab8535..a140ea707 100644 --- a/examples/example-app-router/src/app/[locale]/pathnames/page.tsx +++ b/examples/example-app-router/src/app/[locale]/pathnames/page.tsx @@ -1,13 +1,15 @@ import {Locale, useTranslations} from 'next-intl'; import {setRequestLocale} from 'next-intl/server'; +import {use} from 'react'; import PageLayout from '@/components/PageLayout'; type Props = { - params: {locale: Locale}; + params: Promise<{locale: Locale}>; }; -export default function PathnamesPage({params: {locale}}: Props) { +export default function PathnamesPage({params}: Props) { // Enable static rendering + const {locale} = use(params); setRequestLocale(locale); const t = useTranslations('PathnamesPage'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95bf8025b..5442e4883 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,13 +40,13 @@ importers: version: 2.1.5(react@18.3.1) '@vercel/analytics': specifier: 1.3.1 - version: 1.3.1(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 1.3.1(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@vercel/og': specifier: ^0.6.3 version: 0.6.3 '@vercel/speed-insights': specifier: ^1.0.12 - version: 1.0.13(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 1.0.13(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -58,10 +58,10 @@ importers: version: 14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nextra: specifier: ^3.1.0 - version: 3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + version: 3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) nextra-theme-docs: specifier: ^3.1.0 - version: 3.1.0(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.1.0(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -86,13 +86,13 @@ importers: version: 18.3.12 autoprefixer: specifier: ^10.4.19 - version: 10.4.20(postcss@8.4.39) + version: 10.4.20(postcss@8.4.47) eslint: specifier: ^9.11.1 version: 9.13.0(jiti@2.3.3) eslint-config-molindo: specifier: ^8.0.0 - version: 8.0.0(@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.9.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(@typescript-eslint/parser@8.9.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(jest@29.7.0(@types/node@20.17.0))(tailwindcss@3.4.14)(typescript@5.6.3)(vitest@2.1.3(@edge-runtime/vm@4.0.3)(@types/node@20.17.0)(jsdom@25.0.1)(terser@5.36.0)) + version: 8.0.0(@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(jest@29.7.0(@types/node@20.17.0))(tailwindcss@3.4.14)(typescript@5.6.3)(vitest@2.1.3(@edge-runtime/vm@4.0.3)(@types/node@20.17.0)(jsdom@25.0.1)(terser@5.36.0)) fast-glob: specifier: ^3.3.2 version: 3.3.2 @@ -101,7 +101,7 @@ importers: version: 15.11.0 next-sitemap: specifier: ^4.2.3 - version: 4.2.3(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 4.2.3(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) next-validate-link: specifier: ^1.3.0 version: 1.3.0 @@ -118,8 +118,8 @@ importers: specifier: ^2.1.1 version: 2.1.1 next: - specifier: ^14.2.4 - version: 14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 15.0.4-canary.5 + version: 15.0.4-canary.5(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-intl: specifier: ^3.0.0 version: link:../../packages/next-intl @@ -162,7 +162,7 @@ importers: version: 9.13.0(jiti@2.3.3) eslint-config-molindo: specifier: ^8.0.0 - version: 8.0.0(@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(jest@29.7.0(@types/node@20.17.0))(tailwindcss@3.4.14)(typescript@5.6.3)(vitest@2.1.3(@edge-runtime/vm@4.0.3)(@types/node@20.17.0)(jsdom@25.0.1)(terser@5.36.0)) + version: 8.0.0(@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.9.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(@typescript-eslint/parser@8.9.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(jest@29.7.0(@types/node@20.17.0))(tailwindcss@3.4.14)(typescript@5.6.3)(vitest@2.1.3(@edge-runtime/vm@4.0.3)(@types/node@20.17.0)(jsdom@25.0.1)(terser@5.36.0)) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.17.0) @@ -269,7 +269,7 @@ importers: version: 14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-auth: specifier: ^4.24.7 - version: 4.24.8(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.24.8(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-intl: specifier: ^3.0.0 version: link:../../packages/next-intl @@ -620,7 +620,7 @@ importers: dependencies: next: specifier: ^12.0.0 - version: 12.3.4(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + version: 12.3.4(@babel/core@7.25.9)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) react: specifier: ^17.0.0 version: 17.0.2 @@ -2749,7 +2749,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {'0': node >=0.10.0} + engines: {node: '>=0.10.0'} '@expo/cli@0.4.11': resolution: {integrity: sha512-L9Ci9RBh0aPFEDF1AjDYPk54OgeUJIKzxF3lRgITm+lQpI3IEKjAc9LaYeQeO1mlZMUQmPkHArF8iyz1eOeVoQ==} @@ -3388,6 +3388,9 @@ packages: '@next/env@14.2.16': resolution: {integrity: sha512-fLrX5TfJzHCbnZ9YUSnGW63tMV3L4nSfhgOQ0iCcX21Pt+VSTDuaLsSuL8J/2XAiVA5AnzvXDpf6pMs60QxOag==} + '@next/env@15.0.4-canary.5': + resolution: {integrity: sha512-wVj9Gsa2Jlncxp6R74cF7NX7fbb+iuXigg0sPEcfcOTdVYOPNte/Zshyaw6bQf6gF6Jnal0Lx5kAC5i/sN4yOw==} + '@next/mdx@14.2.16': resolution: {integrity: sha512-IVd/Z3vYpIZ/nzqhmSHmTKfSrQ6eZrorkzs0uzMCXt3hL6lw4qf/jcEExZmXoenqnZ5vX0xkou+y4uWAKZhvfw==} peerDependencies: @@ -3423,6 +3426,12 @@ packages: cpu: [arm64] os: [darwin] + '@next/swc-darwin-arm64@15.0.4-canary.5': + resolution: {integrity: sha512-9N6FZd6Cik7+08cT2GDON5l148wqvauw7u75iixqZ1Tz8BBHT70qCbDJGonWGM60PphgDNqBxCXGuJSTBjQHow==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + '@next/swc-darwin-x64@12.3.4': resolution: {integrity: sha512-PPF7tbWD4k0dJ2EcUSnOsaOJ5rhT3rlEt/3LhZUGiYNL8KvoqczFrETlUx0cUYaXe11dRA3F80Hpt727QIwByQ==} engines: {node: '>= 10'} @@ -3435,6 +3444,12 @@ packages: cpu: [x64] os: [darwin] + '@next/swc-darwin-x64@15.0.4-canary.5': + resolution: {integrity: sha512-Rox0ihBOKV1Er7AGZj+q/WhNemobEsXMTwrwyELza9VoAoOBH6XJXD+/KSCTXKcWLont+xiJK7gxsZkc+rYR+w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + '@next/swc-freebsd-x64@12.3.4': resolution: {integrity: sha512-KM9JXRXi/U2PUM928z7l4tnfQ9u8bTco/jb939pdFUHqc28V43Ohd31MmZD1QzEK4aFlMRaIBQOWQZh4D/E5lQ==} engines: {node: '>= 10'} @@ -3459,6 +3474,12 @@ packages: cpu: [arm64] os: [linux] + '@next/swc-linux-arm64-gnu@15.0.4-canary.5': + resolution: {integrity: sha512-S+BjJl3lLEsiuMadXXHWgzL3A/RYXiv8Ngx2IbmWrCVIEGBfnXWJcbGFVN4PwPUiLK8Ymdg0mqgybOSUcHG4Ng==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + '@next/swc-linux-arm64-musl@12.3.4': resolution: {integrity: sha512-EETZPa1juczrKLWk5okoW2hv7D7WvonU+Cf2CgsSoxgsYbUCZ1voOpL4JZTOb6IbKMDo6ja+SbY0vzXZBUMvkQ==} engines: {node: '>= 10'} @@ -3471,6 +3492,12 @@ packages: cpu: [arm64] os: [linux] + '@next/swc-linux-arm64-musl@15.0.4-canary.5': + resolution: {integrity: sha512-OMt5j/zVueGy4vr2nYvsApjedt5vcDOnQ3V5s96jrpvNnSgPknmLYe5nZo7EXpnEnIWqNr7KqqwQuVvXoqUr1w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + '@next/swc-linux-x64-gnu@12.3.4': resolution: {integrity: sha512-4csPbRbfZbuWOk3ATyWcvVFdD9/Rsdq5YHKvRuEni68OCLkfy4f+4I9OBpyK1SKJ00Cih16NJbHE+k+ljPPpag==} engines: {node: '>= 10'} @@ -3483,6 +3510,12 @@ packages: cpu: [x64] os: [linux] + '@next/swc-linux-x64-gnu@15.0.4-canary.5': + resolution: {integrity: sha512-/2wQvZHDYfL4xqp5ltl6eHLr3tlj587WwpVtqZjGMJ2grihGMb9CrK5MxUXqgzxUBTjMRR2OtRuJo9A6rPvm8Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + '@next/swc-linux-x64-musl@12.3.4': resolution: {integrity: sha512-YeBmI+63Ro75SUiL/QXEVXQ19T++58aI/IINOyhpsRL1LKdyfK/35iilraZEFz9bLQrwy1LYAR5lK200A9Gjbg==} engines: {node: '>= 10'} @@ -3495,6 +3528,12 @@ packages: cpu: [x64] os: [linux] + '@next/swc-linux-x64-musl@15.0.4-canary.5': + resolution: {integrity: sha512-dncDPb0cAnNHxBiI5Yon9zm3Aw83fZJybekQeA6b5P+YNR69IZnRYmpDUxgfwNPUyV5fh2kqEAAFH4ZmlQSmCw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + '@next/swc-win32-arm64-msvc@12.3.4': resolution: {integrity: sha512-Sd0qFUJv8Tj0PukAYbCCDbmXcMkbIuhnTeHm9m4ZGjCf6kt7E/RMs55Pd3R5ePjOkN7dJEuxYBehawTR/aPDSQ==} engines: {node: '>= 10'} @@ -3507,6 +3546,12 @@ packages: cpu: [arm64] os: [win32] + '@next/swc-win32-arm64-msvc@15.0.4-canary.5': + resolution: {integrity: sha512-WtTJ2PuzLPkq4PTWik6ZK+jdY2SDpkIXkRwnB9K501GS7f5wvl/OExO81r+StwlKnFjHBq5MSsPVNAJfIKjdnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + '@next/swc-win32-ia32-msvc@12.3.4': resolution: {integrity: sha512-rt/vv/vg/ZGGkrkKcuJ0LyliRdbskQU+91bje+PgoYmxTZf/tYs6IfbmgudBJk6gH3QnjHWbkphDdRQrseRefQ==} engines: {node: '>= 10'} @@ -3531,6 +3576,12 @@ packages: cpu: [x64] os: [win32] + '@next/swc-win32-x64-msvc@15.0.4-canary.5': + resolution: {integrity: sha512-QuaB5A2IQj9K+B2oTl61LYPISYan/x+biwnRD7DbyBEIDWkzJPjMpxKqP62Kkmv91GfhqCql3WlOWbqW+WemRg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -4534,6 +4585,9 @@ packages: '@swc/helpers@0.4.11': resolution: {integrity: sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw==} + '@swc/helpers@0.5.13': + resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} + '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} @@ -10594,6 +10648,27 @@ packages: sass: optional: true + next@15.0.4-canary.5: + resolution: {integrity: sha512-0Xb0tjDadaH3KNonyZnT97+fGOet13WlVAGhTfGLNkz3BZ4GvD88vT7Qx2pkoPgPN0Cr9mxQrNNwpZ0sLbVxHg==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-66855b96-20241106 + react-dom: ^18.2.0 || 19.0.0-rc-66855b96-20241106 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + nextra-theme-docs@3.1.0: resolution: {integrity: sha512-2zAC+xnqLzl/kLYCaoVfdupyA6pD5OgF+4iR3zQiPOzfnwJikPQePnr3SCT+tPPgYVuoqSDA5GNc9DvvAHtefQ==} peerDependencies: @@ -11804,6 +11879,7 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. + (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qrcode-terminal@0.11.0: @@ -17488,6 +17564,8 @@ snapshots: '@next/env@14.2.16': {} + '@next/env@15.0.4-canary.5': {} + '@next/mdx@14.2.16(@mdx-js/loader@3.1.0(webpack@5.95.0(esbuild@0.23.1)))(@mdx-js/react@3.1.0(@types/react@18.3.12)(react@18.3.1))': dependencies: source-map: 0.7.4 @@ -17507,12 +17585,18 @@ snapshots: '@next/swc-darwin-arm64@14.2.16': optional: true + '@next/swc-darwin-arm64@15.0.4-canary.5': + optional: true + '@next/swc-darwin-x64@12.3.4': optional: true '@next/swc-darwin-x64@14.2.16': optional: true + '@next/swc-darwin-x64@15.0.4-canary.5': + optional: true + '@next/swc-freebsd-x64@12.3.4': optional: true @@ -17525,30 +17609,45 @@ snapshots: '@next/swc-linux-arm64-gnu@14.2.16': optional: true + '@next/swc-linux-arm64-gnu@15.0.4-canary.5': + optional: true + '@next/swc-linux-arm64-musl@12.3.4': optional: true '@next/swc-linux-arm64-musl@14.2.16': optional: true + '@next/swc-linux-arm64-musl@15.0.4-canary.5': + optional: true + '@next/swc-linux-x64-gnu@12.3.4': optional: true '@next/swc-linux-x64-gnu@14.2.16': optional: true + '@next/swc-linux-x64-gnu@15.0.4-canary.5': + optional: true + '@next/swc-linux-x64-musl@12.3.4': optional: true '@next/swc-linux-x64-musl@14.2.16': optional: true + '@next/swc-linux-x64-musl@15.0.4-canary.5': + optional: true + '@next/swc-win32-arm64-msvc@12.3.4': optional: true '@next/swc-win32-arm64-msvc@14.2.16': optional: true + '@next/swc-win32-arm64-msvc@15.0.4-canary.5': + optional: true + '@next/swc-win32-ia32-msvc@12.3.4': optional: true @@ -17561,6 +17660,9 @@ snapshots: '@next/swc-win32-x64-msvc@14.2.16': optional: true + '@next/swc-win32-x64-msvc@15.0.4-canary.5': + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -18137,7 +18239,7 @@ snapshots: '@react-aria/ssr@3.9.6(react@18.3.1)': dependencies: - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.13 react: 18.3.1 '@react-aria/utils@3.25.3(react@18.3.1)': @@ -18145,7 +18247,7 @@ snapshots: '@react-aria/ssr': 3.9.6(react@18.3.1) '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.13 clsx: 2.1.1 react: 18.3.1 @@ -18317,7 +18419,7 @@ snapshots: '@react-stately/utils@3.10.4(react@18.3.1)': dependencies: - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.13 react: 18.3.1 '@react-types/shared@3.25.0(react@18.3.1)': @@ -18958,6 +19060,10 @@ snapshots: dependencies: tslib: 2.8.0 + '@swc/helpers@0.5.13': + dependencies: + tslib: 2.8.0 + '@swc/helpers@0.5.5': dependencies: '@swc/counter': 0.1.3 @@ -19590,7 +19696,7 @@ snapshots: '@vanilla-extract/private@1.0.3': {} - '@vercel/analytics@1.3.1(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@vercel/analytics@1.3.1(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': dependencies: server-only: 0.0.1 optionalDependencies: @@ -19603,7 +19709,7 @@ snapshots: satori: 0.10.9 yoga-wasm-web: 0.3.3 - '@vercel/speed-insights@1.0.13(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@vercel/speed-insights@1.0.13(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': optionalDependencies: next: 14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -20279,16 +20385,6 @@ snapshots: atob@2.1.2: {} - autoprefixer@10.4.20(postcss@8.4.39): - dependencies: - browserslist: 4.24.0 - caniuse-lite: 1.0.30001669 - fraction.js: 4.3.7 - normalize-range: 0.1.2 - picocolors: 1.0.1 - postcss: 8.4.39 - postcss-value-parser: 4.2.0 - autoprefixer@10.4.20(postcss@8.4.47): dependencies: browserslist: 4.24.0 @@ -27445,7 +27541,7 @@ snapshots: dependencies: type-fest: 2.19.0 - next-auth@4.24.8(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-auth@4.24.8(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.24.7 '@panva/hkdf': 1.2.0 @@ -27460,7 +27556,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) uuid: 8.3.2 - next-sitemap@4.2.3(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + next-sitemap@4.2.3(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: '@corex/deepmerge': 4.0.43 '@next/env': 13.5.6 @@ -27484,7 +27580,7 @@ snapshots: transitivePeerDependencies: - supports-color - next@12.3.4(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + next@12.3.4(@babel/core@7.25.9)(react-dom@17.0.2(react@17.0.2))(react@17.0.2): dependencies: '@next/env': 12.3.4 '@swc/helpers': 0.4.11 @@ -27492,7 +27588,7 @@ snapshots: postcss: 8.4.14 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - styled-jsx: 5.0.7(react@17.0.2) + styled-jsx: 5.0.7(@babel/core@7.25.9)(react@17.0.2) use-sync-external-store: 1.2.0(react@17.0.2) optionalDependencies: '@next/swc-android-arm-eabi': 12.3.4 @@ -27538,7 +27634,33 @@ snapshots: - '@babel/core' - babel-plugin-macros - nextra-theme-docs@3.1.0(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@15.0.4-canary.5(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 15.0.4-canary.5 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.13 + busboy: 1.6.0 + caniuse-lite: 1.0.30001669 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.6(@babel/core@7.25.9)(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 15.0.4-canary.5 + '@next/swc-darwin-x64': 15.0.4-canary.5 + '@next/swc-linux-arm64-gnu': 15.0.4-canary.5 + '@next/swc-linux-arm64-musl': 15.0.4-canary.5 + '@next/swc-linux-x64-gnu': 15.0.4-canary.5 + '@next/swc-linux-x64-musl': 15.0.4-canary.5 + '@next/swc-win32-arm64-msvc': 15.0.4-canary.5 + '@next/swc-win32-x64-msvc': 15.0.4-canary.5 + '@playwright/test': 1.48.1 + sharp: 0.33.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + nextra-theme-docs@3.1.0(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@headlessui/react': 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) clsx: 2.1.1 @@ -27546,13 +27668,13 @@ snapshots: flexsearch: 0.7.43 next: 14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - nextra: 3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + nextra: 3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) scroll-into-view-if-needed: 3.1.0 zod: 3.23.8 - nextra@3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3): + nextra@3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3): dependencies: '@formatjs/intl-localematcher': 0.5.5 '@headlessui/react': 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -30630,9 +30752,11 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-jsx@5.0.7(react@17.0.2): + styled-jsx@5.0.7(@babel/core@7.25.9)(react@17.0.2): dependencies: react: 17.0.2 + optionalDependencies: + '@babel/core': 7.25.9 styled-jsx@5.1.1(@babel/core@7.25.9)(react@18.3.1): dependencies: From 550a734179eca634d13262dd79a344c8f407aef4 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Mon, 11 Nov 2024 13:46:25 +0100 Subject: [PATCH 03/28] dynamicIO, ppr, remove dynamicParams = false --- examples/example-app-router/next.config.mjs | 7 ++++- .../src/app/[locale]/layout.tsx | 31 ++++++++++++++----- .../src/app/[locale]/page.tsx | 14 ++------- .../src/app/[locale]/pathnames/page.tsx | 14 ++------- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/examples/example-app-router/next.config.mjs b/examples/example-app-router/next.config.mjs index 1751fe61a..8df840b65 100644 --- a/examples/example-app-router/next.config.mjs +++ b/examples/example-app-router/next.config.mjs @@ -9,6 +9,11 @@ const withNextIntl = createNextIntlPlugin({ }); /** @type {import('next').NextConfig} */ -const config = {}; +const config = { + experimental: { + dynamicIO: true, + ppr: true + } +}; export default withNextIntl(config); diff --git a/examples/example-app-router/src/app/[locale]/layout.tsx b/examples/example-app-router/src/app/[locale]/layout.tsx index 02ada88c6..4518afed6 100644 --- a/examples/example-app-router/src/app/[locale]/layout.tsx +++ b/examples/example-app-router/src/app/[locale]/layout.tsx @@ -1,6 +1,7 @@ -import {Locale} from 'next-intl'; -import {getTranslations, setRequestLocale} from 'next-intl/server'; -import {ReactNode} from 'react'; +import {notFound} from 'next/navigation'; +import {Locale, hasLocale} from 'next-intl'; +import {getTranslations} from 'next-intl/server'; +import {ReactNode, Suspense} from 'react'; import BaseLayout from '@/components/BaseLayout'; import {routing} from '@/i18n/routing'; @@ -22,13 +23,27 @@ export async function generateMetadata({params}: Omit) { }; } -// Return a 404 for unknown locales -export const dynamicParams = false; +export default function LocaleLayoutMain(props: Props) { + return ( + + +
Loading …
+ + + } + > + +
+ ); +} -export default async function LocaleLayout({children, params}: Props) { - // Enable static rendering +async function LocaleLayout({children, params}: Props) { const {locale} = await params; - setRequestLocale(locale); + if (!hasLocale(routing.locales, locale)) { + notFound(); + } return {children}; } diff --git a/examples/example-app-router/src/app/[locale]/page.tsx b/examples/example-app-router/src/app/[locale]/page.tsx index 8f2dc3165..c3a9bd142 100644 --- a/examples/example-app-router/src/app/[locale]/page.tsx +++ b/examples/example-app-router/src/app/[locale]/page.tsx @@ -1,17 +1,7 @@ -import {Locale, useTranslations} from 'next-intl'; -import {setRequestLocale} from 'next-intl/server'; -import {use} from 'react'; +import {useTranslations} from 'next-intl'; import PageLayout from '@/components/PageLayout'; -type Props = { - params: Promise<{locale: Locale}>; -}; - -export default function IndexPage({params}: Props) { - // Enable static rendering - const {locale} = use(params); - setRequestLocale(locale); - +export default function IndexPage() { const t = useTranslations('IndexPage'); return ( diff --git a/examples/example-app-router/src/app/[locale]/pathnames/page.tsx b/examples/example-app-router/src/app/[locale]/pathnames/page.tsx index a140ea707..ecadf9581 100644 --- a/examples/example-app-router/src/app/[locale]/pathnames/page.tsx +++ b/examples/example-app-router/src/app/[locale]/pathnames/page.tsx @@ -1,17 +1,7 @@ -import {Locale, useTranslations} from 'next-intl'; -import {setRequestLocale} from 'next-intl/server'; -import {use} from 'react'; +import {useTranslations} from 'next-intl'; import PageLayout from '@/components/PageLayout'; -type Props = { - params: Promise<{locale: Locale}>; -}; - -export default function PathnamesPage({params}: Props) { - // Enable static rendering - const {locale} = use(params); - setRequestLocale(locale); - +export default function PathnamesPage() { const t = useTranslations('PathnamesPage'); return ( From 096033a7265f885ce53a5dcb4fe4bb18c68b4f11 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Tue, 12 Nov 2024 11:35:32 +0100 Subject: [PATCH 04/28] read locale from `getLocale`, use hypothetical api to read params deeply, static messages import --- .../src/app/[locale]/layout.tsx | 33 +++++-------------- .../example-app-router/src/i18n/future.ts | 13 ++++++++ .../example-app-router/src/i18n/request.ts | 19 +++++++---- 3 files changed, 34 insertions(+), 31 deletions(-) create mode 100644 examples/example-app-router/src/i18n/future.ts diff --git a/examples/example-app-router/src/app/[locale]/layout.tsx b/examples/example-app-router/src/app/[locale]/layout.tsx index 4518afed6..1cde913ec 100644 --- a/examples/example-app-router/src/app/[locale]/layout.tsx +++ b/examples/example-app-router/src/app/[locale]/layout.tsx @@ -1,46 +1,29 @@ import {notFound} from 'next/navigation'; -import {Locale, hasLocale} from 'next-intl'; -import {getTranslations} from 'next-intl/server'; -import {ReactNode, Suspense} from 'react'; +import {hasLocale} from 'next-intl'; +import {getLocale, getTranslations} from 'next-intl/server'; +import {ReactNode} from 'react'; import BaseLayout from '@/components/BaseLayout'; import {routing} from '@/i18n/routing'; type Props = { children: ReactNode; - params: Promise<{locale: Locale}>; }; export function generateStaticParams() { return routing.locales.map((locale) => ({locale})); } -export async function generateMetadata({params}: Omit) { - const {locale} = await params; - const t = await getTranslations({locale, namespace: 'LocaleLayout'}); +export async function generateMetadata() { + const t = await getTranslations('LocaleLayout'); return { title: t('title') }; } -export default function LocaleLayoutMain(props: Props) { - return ( - - -
Loading …
- - - } - > - -
- ); -} - -async function LocaleLayout({children, params}: Props) { - const {locale} = await params; +export default async function LocaleLayout({children}: Props) { + // This is only necessary as long as there's no `dynamicParams = false` + const locale = await getLocale(); if (!hasLocale(routing.locales, locale)) { notFound(); } diff --git a/examples/example-app-router/src/i18n/future.ts b/examples/example-app-router/src/i18n/future.ts new file mode 100644 index 000000000..7568aa23d --- /dev/null +++ b/examples/example-app-router/src/i18n/future.ts @@ -0,0 +1,13 @@ +export async function rootParams(): Promise<{ + locale?: string; +}> { + // Outside of `[locale]` + // return Promise.resolve({ + // locale: undefined + // }); + + // In `[locale]` + return Promise.resolve({ + locale: 'en' + }); +} diff --git a/examples/example-app-router/src/i18n/request.ts b/examples/example-app-router/src/i18n/request.ts index 370fc6d0c..cb01d915b 100644 --- a/examples/example-app-router/src/i18n/request.ts +++ b/examples/example-app-router/src/i18n/request.ts @@ -1,16 +1,23 @@ import {hasLocale} from 'next-intl'; import {getRequestConfig} from 'next-intl/server'; +import messages from '../../messages/en.json'; +import {rootParams} from './future'; 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 +async function now() { + 'use cache'; + return new Date(); +} + +export default getRequestConfig(async () => { + const params = await rootParams(); + const locale = hasLocale(routing.locales, params.locale) + ? params.locale : routing.defaultLocale; return { locale, - messages: (await import(`../../messages/${locale}.json`)).default + messages, + now: await now() }; }); From e78099d3d8e7473170c6c32701f0536b50a1cab6 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Tue, 12 Nov 2024 15:28:20 +0100 Subject: [PATCH 05/28] Lazy-init `now` when `useNow` or `format.relativeTime` is used in components --- .../src/react-server/useTranslations.tsx | 4 +-- .../src/server/react-server/getConfig.tsx | 16 +++++----- .../react-server/getTranslations.test.tsx | 29 +++++++++++++++++++ .../server/react-server/getTranslations.tsx | 8 ++--- .../react-server/getTranslator.tsx | 11 ++++++- 5 files changed, 52 insertions(+), 16 deletions(-) create mode 100644 packages/next-intl/src/server/react-server/getTranslations.test.tsx rename packages/next-intl/src/{ => server}/react-server/getTranslator.tsx (52%) diff --git a/packages/next-intl/src/react-server/useTranslations.tsx b/packages/next-intl/src/react-server/useTranslations.tsx index 3968c5902..a8442631d 100644 --- a/packages/next-intl/src/react-server/useTranslations.tsx +++ b/packages/next-intl/src/react-server/useTranslations.tsx @@ -1,10 +1,10 @@ import type {useTranslations as useTranslationsType} from 'use-intl'; -import getBaseTranslator from './getTranslator.tsx'; +import getTranslator from '../server/react-server/getTranslator.tsx'; import useConfig from './useConfig.tsx'; export default function useTranslations( ...[namespace]: Parameters ): ReturnType { const config = useConfig('useTranslations'); - return getBaseTranslator(config, namespace); + return getTranslator(config, namespace); } diff --git a/packages/next-intl/src/server/react-server/getConfig.tsx b/packages/next-intl/src/server/react-server/getConfig.tsx index a5c2a85bb..26ddb56d3 100644 --- a/packages/next-intl/src/server/react-server/getConfig.tsx +++ b/packages/next-intl/src/server/react-server/getConfig.tsx @@ -65,12 +65,7 @@ See also: https://next-intl-docs.vercel.app/docs/usage/configuration#i18n-reques ); } - return { - ...result, - locale: result.locale, - now: result.now || getDefaultNow(), - timeZone: result.timeZone || getDefaultTimeZone() - }; + return result; } const receiveRuntimeConfig = cache(receiveRuntimeConfigImpl); @@ -92,7 +87,14 @@ async function getConfigImpl(localeOverride?: Locale): Promise< ); return { ...initializeConfig(runtimeConfig), - _formatters: getFormatters(getCache()) + _formatters: getFormatters(getCache()), + timeZone: runtimeConfig.timeZone || getDefaultTimeZone(), + + // Only init when necessary to avoid triggering a `dynamicIO` error + // (i.e. when using `format.relativeTime` or `useNow`) + get now() { + return runtimeConfig.now ?? getDefaultNow(); + } }; } const getConfig = cache(getConfigImpl); diff --git a/packages/next-intl/src/server/react-server/getTranslations.test.tsx b/packages/next-intl/src/server/react-server/getTranslations.test.tsx new file mode 100644 index 000000000..5a19977c5 --- /dev/null +++ b/packages/next-intl/src/server/react-server/getTranslations.test.tsx @@ -0,0 +1,29 @@ +import {createTranslator} from 'use-intl/core'; +import {expect, it, vi} from 'vitest'; +import getTranslations from './getTranslations.tsx'; + +vi.mock('react'); +vi.mock('use-intl/core'); + +vi.mock('next-intl/config', () => ({ + default: async () => + ( + (await vi.importActual('../../../src/server/react-server')) as any + ).getRequestConfig({ + locale: 'en', + timeZone: 'Europe/London', + messages: { + title: 'Hello' + } + }) +})); + +it('should not include `now` in the translator config', async () => { + await getTranslations(); + + expect(createTranslator).toHaveBeenCalledWith( + expect.not.objectContaining({ + now: expect.anything() + }) + ); +}); diff --git a/packages/next-intl/src/server/react-server/getTranslations.tsx b/packages/next-intl/src/server/react-server/getTranslations.tsx index e3d5d8c68..9b4047bf2 100644 --- a/packages/next-intl/src/server/react-server/getTranslations.tsx +++ b/packages/next-intl/src/server/react-server/getTranslations.tsx @@ -7,6 +7,7 @@ import { createTranslator } from 'use-intl/core'; import getConfig from './getConfig.tsx'; +import getTranslator from './getTranslator.tsx'; // Maintainer note: `getTranslations` has two different call signatures. // We need to define these with function overloads, otherwise TypeScript @@ -40,12 +41,7 @@ async function getTranslations< } const config = await getConfig(locale); - - return createTranslator({ - ...config, - namespace, - messages: config.messages - }); + return getTranslator(config, namespace); } export default cache(getTranslations); diff --git a/packages/next-intl/src/react-server/getTranslator.tsx b/packages/next-intl/src/server/react-server/getTranslator.tsx similarity index 52% rename from packages/next-intl/src/react-server/getTranslator.tsx rename to packages/next-intl/src/server/react-server/getTranslator.tsx index 3b4d7f50c..9ef684cd1 100644 --- a/packages/next-intl/src/react-server/getTranslator.tsx +++ b/packages/next-intl/src/server/react-server/getTranslator.tsx @@ -13,8 +13,17 @@ function getTranslatorImpl< namespace?: NestedKey ): ReturnType> { return createTranslator({ - ...config, + locale: config.locale, + _cache: config._cache, + _formatters: config._formatters, + formats: config.formats, + getMessageFallback: config.getMessageFallback, + messages: config.messages, + onError: config.onError, + timeZone: config.timeZone, namespace + // We don't pass `now` here because a) it's not needed and b) it might + // require reading the current time, which causes an error with `dynamicIO` }); } From 7c528cf1bdbe1e2c64f96bfc84d3ca8faa4159be Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Tue, 12 Nov 2024 17:38:24 +0100 Subject: [PATCH 06/28] Don't create default value for `now` when inheriting in `NextIntlClientProvider`, adjust docs --- docs/src/pages/docs/usage/configuration.mdx | 23 +++++++++++-------- docs/src/pages/docs/usage/dates-times.mdx | 6 ++--- .../NextIntlClientProviderServer.tsx | 6 +++-- .../src/react-server/useFormatter.tsx | 11 ++++++++- .../next-intl/src/react-server/useNow.tsx | 3 ++- .../src/server/react-server/getConfig.tsx | 15 +----------- .../src/server/react-server/getConfigNow.tsx | 11 +++++++++ .../src/server/react-server/getDefaultNow.tsx | 8 +++++++ .../src/server/react-server/getFormats.tsx | 4 ++-- .../src/server/react-server/getNow.tsx | 12 +++------- .../src/server/react-server/getTranslator.tsx | 11 +-------- .../use-intl/src/core/createFormatter.tsx | 2 +- 12 files changed, 59 insertions(+), 53 deletions(-) create mode 100644 packages/next-intl/src/server/react-server/getConfigNow.tsx create mode 100644 packages/next-intl/src/server/react-server/getDefaultNow.tsx diff --git a/docs/src/pages/docs/usage/configuration.mdx b/docs/src/pages/docs/usage/configuration.mdx index 993ad6af7..de9badc9e 100644 --- a/docs/src/pages/docs/usage/configuration.mdx +++ b/docs/src/pages/docs/usage/configuration.mdx @@ -218,7 +218,7 @@ import {getLocale} from 'next-intl/server'; const locale = await getLocale(); ``` -### `Locale` [#locale-type] +### `Locale` type [#locale-type] When passing a `locale` to another function, you can use the `Locale` type for the receiving parameter: @@ -453,7 +453,7 @@ const timeZone = await getTimeZone(); When formatting [relative dates and times](/docs/usage/dates-times#relative-times), `next-intl` will format times in relation to a reference point in time that is referred to as "now". By default, this is the time a component renders. -If you prefer to override the default, you can provide an explicit value for `now`: +If you prefer to provide a global default, you can configure an explicit value for `now`: @@ -461,24 +461,29 @@ If you prefer to override the default, you can provide an explicit value for `no ```tsx filename="i18n/request.ts" import {getRequestConfig} from 'next-intl/server'; +function now() { + 'use cache'; + + // Use this value consistently + return new Date(); +} + export default getRequestConfig(async () => { return { - // This is the default, a single date instance will be - // used by all Server Components to ensure consistency. - // Tip: This value can be mocked to a constant value - // for consistent results in end-to-end-tests. - now: new Date() + now: now() // ... }; }); ``` +If a `now` value is provided in `i18n/request.ts`, this will automatically be inherited by Client Components if you wrap them in a `NextIntlClientProvider` that is rendered by a Server Component. + ```tsx -const now = new Date('2020-11-20T10:36:01.516Z'); +const now = new Date(); ...; ``` @@ -486,8 +491,6 @@ const now = new Date('2020-11-20T10:36:01.516Z'); -Similarly to the `timeZone`, the `now` value in Client Components is automatically inherited from the server side if you wrap the relevant components in a `NextIntlClientProvider` that is rendered by a Server Component. - ### `useNow` & `getNow` [#use-now] The configured `now` value can be read in components via `useNow` or `getNow`: diff --git a/docs/src/pages/docs/usage/dates-times.mdx b/docs/src/pages/docs/usage/dates-times.mdx index a8627c853..b20409940 100644 --- a/docs/src/pages/docs/usage/dates-times.mdx +++ b/docs/src/pages/docs/usage/dates-times.mdx @@ -77,7 +77,7 @@ Note that values are rounded, so e.g. if 126 minutes have passed, "2 hours ago" ### Supplying `now` -By default, `relativeTime` will use [the global value for `now`](/docs/usage/configuration#now). If you want to use a different value, you can explicitly pass this as the second parameter. +By default, `relativeTime` will use the [global value for `now`](/docs/usage/configuration#now). If you want to use a different value, you can explicitly pass this as the second parameter. ```js import {useFormatter} from 'next-intl'; @@ -92,7 +92,7 @@ function Component() { } ``` -If you want the relative time value to update over time, you can do so with [the `useNow` hook](/docs/usage/configuration#now): +In case you want the relative time value to update over time, you can do so with [the `useNow` hook](/docs/usage/configuration#now): ```js import {useNow, useFormatter} from 'next-intl'; @@ -114,7 +114,7 @@ function Component() { ### Customizing the unit -By default, `relativeTime` will pick a unit based on the difference between the passed date and `now` (e.g. 3 seconds, 40 minutes, 4 days, etc.). +By default, `relativeTime` will pick a unit based on the difference between the passed date and `now` like "3 seconds" or "5 days". If you want to use a specific unit, you can provide options via the second argument: diff --git a/packages/next-intl/src/react-server/NextIntlClientProviderServer.tsx b/packages/next-intl/src/react-server/NextIntlClientProviderServer.tsx index a17ac2e62..541375fce 100644 --- a/packages/next-intl/src/react-server/NextIntlClientProviderServer.tsx +++ b/packages/next-intl/src/react-server/NextIntlClientProviderServer.tsx @@ -1,6 +1,7 @@ import {ComponentProps} from 'react'; +import getConfigNow from '../server/react-server/getConfigNow.tsx'; import getFormats from '../server/react-server/getFormats.tsx'; -import {getLocale, getNow, getTimeZone} from '../server.react-server.tsx'; +import {getLocale, getTimeZone} from '../server.react-server.tsx'; import BaseNextIntlClientProvider from '../shared/NextIntlClientProvider.tsx'; type Props = ComponentProps; @@ -18,7 +19,8 @@ export default async function NextIntlClientProviderServer({ // See https://github.com/amannn/next-intl/issues/631 formats={formats === undefined ? await getFormats() : formats} locale={locale ?? (await getLocale())} - now={now ?? (await getNow())} + // Note that don't assign a default for `now` here + now={now ?? (await getConfigNow())} timeZone={timeZone ?? (await getTimeZone())} {...rest} /> diff --git a/packages/next-intl/src/react-server/useFormatter.tsx b/packages/next-intl/src/react-server/useFormatter.tsx index 48be0f27e..567d6d12d 100644 --- a/packages/next-intl/src/react-server/useFormatter.tsx +++ b/packages/next-intl/src/react-server/useFormatter.tsx @@ -1,11 +1,20 @@ import {cache} from 'react'; import type {useFormatter as useFormatterType} from 'use-intl'; import {createFormatter} from 'use-intl/core'; +import getDefaultNow from '../server/react-server/getDefaultNow.tsx'; import useConfig from './useConfig.tsx'; const createFormatterCached = cache(createFormatter); export default function useFormatter(): ReturnType { const config = useConfig('useFormatter'); - return createFormatterCached(config); + + return createFormatterCached({ + ...config, + // Only init when necessary to avoid triggering a `dynamicIO` error + // unnecessarily (`now` is only needed for `format.relativeTime`) + get now() { + return config.now ?? getDefaultNow(); + } + }); } diff --git a/packages/next-intl/src/react-server/useNow.tsx b/packages/next-intl/src/react-server/useNow.tsx index e5f210dc8..3f103db46 100644 --- a/packages/next-intl/src/react-server/useNow.tsx +++ b/packages/next-intl/src/react-server/useNow.tsx @@ -1,4 +1,5 @@ import type {useNow as useNowType} from 'use-intl'; +import getDefaultNow from '../server/react-server/getDefaultNow.tsx'; import useConfig from './useConfig.tsx'; export default function useNow( @@ -11,5 +12,5 @@ export default function useNow( } const config = useConfig('useNow'); - return config.now; + return config.now ?? getDefaultNow(); } diff --git a/packages/next-intl/src/server/react-server/getConfig.tsx b/packages/next-intl/src/server/react-server/getConfig.tsx index 26ddb56d3..495e5423d 100644 --- a/packages/next-intl/src/server/react-server/getConfig.tsx +++ b/packages/next-intl/src/server/react-server/getConfig.tsx @@ -10,12 +10,6 @@ import {getRequestLocale} from './RequestLocale.tsx'; import createRequestConfig from './createRequestConfig.tsx'; import {GetRequestConfigParams} from './getRequestConfig.tsx'; -// Make sure `now` is consistent across the request in case none was configured -function getDefaultNowImpl() { - return new Date(); -} -const getDefaultNow = cache(getDefaultNowImpl); - // This is automatically inherited by `NextIntlClientProvider` if // the component is rendered from a Server Component function getDefaultTimeZoneImpl() { @@ -75,7 +69,6 @@ const getCache = cache(_createCache); async function getConfigImpl(localeOverride?: Locale): Promise< IntlConfig & { getMessageFallback: NonNullable; - now: NonNullable; onError: NonNullable; timeZone: NonNullable; _formatters: ReturnType; @@ -88,13 +81,7 @@ async function getConfigImpl(localeOverride?: Locale): Promise< return { ...initializeConfig(runtimeConfig), _formatters: getFormatters(getCache()), - timeZone: runtimeConfig.timeZone || getDefaultTimeZone(), - - // Only init when necessary to avoid triggering a `dynamicIO` error - // (i.e. when using `format.relativeTime` or `useNow`) - get now() { - return runtimeConfig.now ?? getDefaultNow(); - } + timeZone: runtimeConfig.timeZone || getDefaultTimeZone() }; } const getConfig = cache(getConfigImpl); diff --git a/packages/next-intl/src/server/react-server/getConfigNow.tsx b/packages/next-intl/src/server/react-server/getConfigNow.tsx new file mode 100644 index 000000000..1a0750dc9 --- /dev/null +++ b/packages/next-intl/src/server/react-server/getConfigNow.tsx @@ -0,0 +1,11 @@ +import {cache} from 'react'; +import type {Locale} from 'use-intl'; +import getConfig from './getConfig.tsx'; + +async function getConfigNowImpl(locale?: Locale) { + const config = await getConfig(locale); + return config.now; +} +const getConfigNow = cache(getConfigNowImpl); + +export default getConfigNow; diff --git a/packages/next-intl/src/server/react-server/getDefaultNow.tsx b/packages/next-intl/src/server/react-server/getDefaultNow.tsx new file mode 100644 index 000000000..6d8ee5e14 --- /dev/null +++ b/packages/next-intl/src/server/react-server/getDefaultNow.tsx @@ -0,0 +1,8 @@ +import {cache} from 'react'; + +function getDefaultNowImpl() { + return new Date(); +} +const getDefaultNow = cache(getDefaultNowImpl); + +export default getDefaultNow; diff --git a/packages/next-intl/src/server/react-server/getFormats.tsx b/packages/next-intl/src/server/react-server/getFormats.tsx index d3900eeb9..4532b90ec 100644 --- a/packages/next-intl/src/server/react-server/getFormats.tsx +++ b/packages/next-intl/src/server/react-server/getFormats.tsx @@ -5,6 +5,6 @@ async function getFormatsCachedImpl() { const config = await getConfig(); return config.formats; } -const getFormatsCached = cache(getFormatsCachedImpl); +const getFormats = cache(getFormatsCachedImpl); -export default getFormatsCached; +export default getFormats; diff --git a/packages/next-intl/src/server/react-server/getNow.tsx b/packages/next-intl/src/server/react-server/getNow.tsx index ed39c17f9..8daf796e8 100644 --- a/packages/next-intl/src/server/react-server/getNow.tsx +++ b/packages/next-intl/src/server/react-server/getNow.tsx @@ -1,13 +1,7 @@ -import {cache} from 'react'; import type {Locale} from 'use-intl'; -import getConfig from './getConfig.tsx'; - -async function getNowCachedImpl(locale?: Locale) { - const config = await getConfig(locale); - return config.now; -} -const getNowCached = cache(getNowCachedImpl); +import getConfigNow from './getConfigNow.tsx'; +import getDefaultNow from './getDefaultNow.tsx'; export default async function getNow(opts?: {locale?: Locale}): Promise { - return getNowCached(opts?.locale); + return (await getConfigNow(opts?.locale)) ?? getDefaultNow(); } diff --git a/packages/next-intl/src/server/react-server/getTranslator.tsx b/packages/next-intl/src/server/react-server/getTranslator.tsx index 9ef684cd1..3b4d7f50c 100644 --- a/packages/next-intl/src/server/react-server/getTranslator.tsx +++ b/packages/next-intl/src/server/react-server/getTranslator.tsx @@ -13,17 +13,8 @@ function getTranslatorImpl< namespace?: NestedKey ): ReturnType> { return createTranslator({ - locale: config.locale, - _cache: config._cache, - _formatters: config._formatters, - formats: config.formats, - getMessageFallback: config.getMessageFallback, - messages: config.messages, - onError: config.onError, - timeZone: config.timeZone, + ...config, namespace - // We don't pass `now` here because a) it's not needed and b) it might - // require reading the current time, which causes an error with `dynamicIO` }); } diff --git a/packages/use-intl/src/core/createFormatter.tsx b/packages/use-intl/src/core/createFormatter.tsx index 02c38cf5a..794473bf6 100644 --- a/packages/use-intl/src/core/createFormatter.tsx +++ b/packages/use-intl/src/core/createFormatter.tsx @@ -219,7 +219,7 @@ export default function createFormatter({ new IntlError( IntlErrorCode.ENVIRONMENT_FALLBACK, process.env.NODE_ENV !== 'production' - ? `The \`now\` parameter wasn't provided and there is no global default configured. Consider adding a global default to avoid markup mismatches caused by environment differences. Learn more: https://next-intl-docs.vercel.app/docs/configuration#now` + ? `The \`now\` parameter wasn't provided and there is no global default configured, therefore the current time will be used as a fallback. To avoid markup mismatches caused by environment differences, either provide the \`now\` parameter or configure a global default. Learn more: https://next-intl-docs.vercel.app/docs/configuration#now` : undefined ) ); From 246e8282067eb9db821f98eea6d77a739d697ea1 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 13 Nov 2024 09:39:58 +0100 Subject: [PATCH 07/28] update test --- packages/use-intl/src/react/useFormatter.test.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/use-intl/src/react/useFormatter.test.tsx b/packages/use-intl/src/react/useFormatter.test.tsx index 88707ba4d..26b7b8a76 100644 --- a/packages/use-intl/src/react/useFormatter.test.tsx +++ b/packages/use-intl/src/react/useFormatter.test.tsx @@ -279,9 +279,7 @@ describe('dateTime', () => { ); const error: IntlError = onError.mock.calls[0][0]; - expect(error.message).toMatch( - "ENVIRONMENT_FALLBACK: The `timeZone` parameter wasn't provided and there is no global default configured." - ); + expect(error.message).toMatch(/^ENVIRONMENT_FALLBACK/); expect(error.code).toBe(IntlErrorCode.ENVIRONMENT_FALLBACK); expect(container.textContent).toBe('11/20/2020'); }); @@ -622,9 +620,7 @@ describe('relativeTime', () => { ); const error: IntlError = onError.mock.calls[0][0]; - expect(error.message).toMatch( - "ENVIRONMENT_FALLBACK: The `now` parameter wasn't provided and there is no global default configured." - ); + expect(error.message).toMatch(/^ENVIRONMENT_FALLBACK/); expect(error.code).toBe(IntlErrorCode.ENVIRONMENT_FALLBACK); }); }); From 76adb07da9e1b0bef561f650f1ea7a3249c3c475 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 13 Nov 2024 09:58:33 +0100 Subject: [PATCH 08/28] fix test --- .../NextIntlClientProviderServer.test.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/next-intl/src/react-server/NextIntlClientProviderServer.test.tsx b/packages/next-intl/src/react-server/NextIntlClientProviderServer.test.tsx index fb40d7b1f..013d5e0fd 100644 --- a/packages/next-intl/src/react-server/NextIntlClientProviderServer.test.tsx +++ b/packages/next-intl/src/react-server/NextIntlClientProviderServer.test.tsx @@ -1,12 +1,12 @@ import {expect, it, vi} from 'vitest'; +import getConfigNow from '../server/react-server/getConfigNow.tsx'; import getFormats from '../server/react-server/getFormats.tsx'; -import {getLocale, getNow, getTimeZone} from '../server.react-server.tsx'; +import {getLocale, getTimeZone} from '../server.react-server.tsx'; import NextIntlClientProvider from '../shared/NextIntlClientProvider.tsx'; import NextIntlClientProviderServer from './NextIntlClientProviderServer.tsx'; vi.mock('../../src/server/react-server', async () => ({ getLocale: vi.fn(async () => 'en-US'), - getNow: vi.fn(async () => new Date('2020-01-01T00:00:00.000Z')), getTimeZone: vi.fn(async () => 'America/New_York') })); @@ -20,6 +20,10 @@ vi.mock('../../src/server/react-server/getFormats', () => ({ })) })); +vi.mock('../../src/server/react-server/getConfigNow', () => ({ + default: vi.fn(async () => new Date('2020-01-01T00:00:00.000Z')) +})); + vi.mock('../../src/shared/NextIntlClientProvider', async () => ({ default: vi.fn(() => 'NextIntlClientProvider') })); @@ -43,7 +47,7 @@ it("doesn't read from headers if all relevant configuration is passed", async () }); expect(getLocale).not.toHaveBeenCalled(); - expect(getNow).not.toHaveBeenCalled(); + expect(getConfigNow).not.toHaveBeenCalled(); expect(getTimeZone).not.toHaveBeenCalled(); expect(getFormats).not.toHaveBeenCalled(); }); @@ -69,7 +73,7 @@ it('reads missing configuration from getter functions', async () => { }); expect(getLocale).toHaveBeenCalled(); - expect(getNow).toHaveBeenCalled(); + expect(getConfigNow).toHaveBeenCalled(); expect(getTimeZone).toHaveBeenCalled(); expect(getFormats).toHaveBeenCalled(); }); From 99a31353d7a700117e7c0dcfde4213511f03fb50 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 13 Nov 2024 09:59:59 +0100 Subject: [PATCH 09/28] fix docs --- docs/src/pages/docs/usage/configuration.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/pages/docs/usage/configuration.mdx b/docs/src/pages/docs/usage/configuration.mdx index de9badc9e..4ded13d4b 100644 --- a/docs/src/pages/docs/usage/configuration.mdx +++ b/docs/src/pages/docs/usage/configuration.mdx @@ -461,7 +461,7 @@ If you prefer to provide a global default, you can configure an explicit value f ```tsx filename="i18n/request.ts" import {getRequestConfig} from 'next-intl/server'; -function now() { +async function now() { 'use cache'; // Use this value consistently @@ -470,7 +470,7 @@ function now() { export default getRequestConfig(async () => { return { - now: now() + now: await now() // ... }; From 7e11418aad8c8c149e444834b0b3212a9f33aeaa Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 13 Nov 2024 10:03:26 +0100 Subject: [PATCH 10/28] fix comment --- .../src/react-server/NextIntlClientProviderServer.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/next-intl/src/react-server/NextIntlClientProviderServer.tsx b/packages/next-intl/src/react-server/NextIntlClientProviderServer.tsx index 541375fce..d3da7c102 100644 --- a/packages/next-intl/src/react-server/NextIntlClientProviderServer.tsx +++ b/packages/next-intl/src/react-server/NextIntlClientProviderServer.tsx @@ -19,7 +19,9 @@ export default async function NextIntlClientProviderServer({ // See https://github.com/amannn/next-intl/issues/631 formats={formats === undefined ? await getFormats() : formats} locale={locale ?? (await getLocale())} - // Note that don't assign a default for `now` here + // Note that we don't assign a default for `now` here, + // we only read one from the request config - if any. + // Otherwise this would cause a `dynamicIO` error. now={now ?? (await getConfigNow())} timeZone={timeZone ?? (await getTimeZone())} {...rest} From a892a44327f899fe30dcb8f92cc6543fedffb314 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 13 Nov 2024 10:22:01 +0100 Subject: [PATCH 11/28] fix test --- examples/example-app-router-playground/src/i18n/request.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/example-app-router-playground/src/i18n/request.tsx b/examples/example-app-router-playground/src/i18n/request.tsx index db7a1b01d..faac058c3 100644 --- a/examples/example-app-router-playground/src/i18n/request.tsx +++ b/examples/example-app-router-playground/src/i18n/request.tsx @@ -45,7 +45,7 @@ export default getRequestConfig(async ({requestLocale}) => { return { locale, - now: now ? new Date(now) : undefined, + now: now ? new Date(now) : new Date(), timeZone, messages, formats, From 36460f18c9a51addbc68a5473ca27a24a43605e4 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 13 Nov 2024 11:03:42 +0100 Subject: [PATCH 12/28] disable manifest for now --- examples/example-app-router/src/app/manifest.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/example-app-router/src/app/manifest.ts b/examples/example-app-router/src/app/manifest.ts index 763fe27aa..4c740a5fe 100644 --- a/examples/example-app-router/src/app/manifest.ts +++ b/examples/example-app-router/src/app/manifest.ts @@ -1,12 +1,11 @@ import {MetadataRoute} from 'next'; -import {getTranslations} from 'next-intl/server'; export default async function manifest(): Promise { - const locale = 'en'; - const t = await getTranslations({locale, namespace: 'Manifest'}); + // const t = await getTranslations('Manifest'); return { - name: t('name'), + // InvariantError: Invariant: Missing Client Reference Manifest for /manifest.webmanifest. This is a bug in Next.js. + // name: t('name'), start_url: '/', theme_color: '#101E33' }; From 80b4aad37eab767bd9a1e25ead2635d63e45f726 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 13 Nov 2024 14:08:45 +0100 Subject: [PATCH 13/28] latest canary --- examples/example-app-router/package.json | 2 +- pnpm-lock.yaml | 140 ++++++++++++----------- 2 files changed, 75 insertions(+), 67 deletions(-) diff --git a/examples/example-app-router/package.json b/examples/example-app-router/package.json index 20293aa44..6fbb3bb7d 100644 --- a/examples/example-app-router/package.json +++ b/examples/example-app-router/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "clsx": "^2.1.1", - "next": "15.0.4-canary.5", + "next": "15.0.4-canary.8", "next-intl": "^3.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5442e4883..a549c6c9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,13 +40,13 @@ importers: version: 2.1.5(react@18.3.1) '@vercel/analytics': specifier: 1.3.1 - version: 1.3.1(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 1.3.1(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@vercel/og': specifier: ^0.6.3 version: 0.6.3 '@vercel/speed-insights': specifier: ^1.0.12 - version: 1.0.13(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 1.0.13(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -58,10 +58,10 @@ importers: version: 14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nextra: specifier: ^3.1.0 - version: 3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + version: 3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) nextra-theme-docs: specifier: ^3.1.0 - version: 3.1.0(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.1.0(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -86,13 +86,13 @@ importers: version: 18.3.12 autoprefixer: specifier: ^10.4.19 - version: 10.4.20(postcss@8.4.47) + version: 10.4.20(postcss@8.4.39) eslint: specifier: ^9.11.1 version: 9.13.0(jiti@2.3.3) eslint-config-molindo: specifier: ^8.0.0 - version: 8.0.0(@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(jest@29.7.0(@types/node@20.17.0))(tailwindcss@3.4.14)(typescript@5.6.3)(vitest@2.1.3(@edge-runtime/vm@4.0.3)(@types/node@20.17.0)(jsdom@25.0.1)(terser@5.36.0)) + version: 8.0.0(@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.9.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(@typescript-eslint/parser@8.9.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(jest@29.7.0(@types/node@20.17.0))(tailwindcss@3.4.14)(typescript@5.6.3)(vitest@2.1.3(@edge-runtime/vm@4.0.3)(@types/node@20.17.0)(jsdom@25.0.1)(terser@5.36.0)) fast-glob: specifier: ^3.3.2 version: 3.3.2 @@ -101,7 +101,7 @@ importers: version: 15.11.0 next-sitemap: specifier: ^4.2.3 - version: 4.2.3(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 4.2.3(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) next-validate-link: specifier: ^1.3.0 version: 1.3.0 @@ -118,8 +118,8 @@ importers: specifier: ^2.1.1 version: 2.1.1 next: - specifier: 15.0.4-canary.5 - version: 15.0.4-canary.5(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 15.0.4-canary.8 + version: 15.0.4-canary.8(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-intl: specifier: ^3.0.0 version: link:../../packages/next-intl @@ -162,7 +162,7 @@ importers: version: 9.13.0(jiti@2.3.3) eslint-config-molindo: specifier: ^8.0.0 - version: 8.0.0(@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.9.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(@typescript-eslint/parser@8.9.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(jest@29.7.0(@types/node@20.17.0))(tailwindcss@3.4.14)(typescript@5.6.3)(vitest@2.1.3(@edge-runtime/vm@4.0.3)(@types/node@20.17.0)(jsdom@25.0.1)(terser@5.36.0)) + version: 8.0.0(@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(eslint@9.13.0(jiti@2.3.3))(jest@29.7.0(@types/node@20.17.0))(tailwindcss@3.4.14)(typescript@5.6.3)(vitest@2.1.3(@edge-runtime/vm@4.0.3)(@types/node@20.17.0)(jsdom@25.0.1)(terser@5.36.0)) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.17.0) @@ -269,7 +269,7 @@ importers: version: 14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-auth: specifier: ^4.24.7 - version: 4.24.8(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.24.8(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-intl: specifier: ^3.0.0 version: link:../../packages/next-intl @@ -620,7 +620,7 @@ importers: dependencies: next: specifier: ^12.0.0 - version: 12.3.4(@babel/core@7.25.9)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + version: 12.3.4(react-dom@17.0.2(react@17.0.2))(react@17.0.2) react: specifier: ^17.0.0 version: 17.0.2 @@ -2749,7 +2749,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {node: '>=0.10.0'} + engines: {'0': node >=0.10.0} '@expo/cli@0.4.11': resolution: {integrity: sha512-L9Ci9RBh0aPFEDF1AjDYPk54OgeUJIKzxF3lRgITm+lQpI3IEKjAc9LaYeQeO1mlZMUQmPkHArF8iyz1eOeVoQ==} @@ -3388,8 +3388,8 @@ packages: '@next/env@14.2.16': resolution: {integrity: sha512-fLrX5TfJzHCbnZ9YUSnGW63tMV3L4nSfhgOQ0iCcX21Pt+VSTDuaLsSuL8J/2XAiVA5AnzvXDpf6pMs60QxOag==} - '@next/env@15.0.4-canary.5': - resolution: {integrity: sha512-wVj9Gsa2Jlncxp6R74cF7NX7fbb+iuXigg0sPEcfcOTdVYOPNte/Zshyaw6bQf6gF6Jnal0Lx5kAC5i/sN4yOw==} + '@next/env@15.0.4-canary.8': + resolution: {integrity: sha512-rQXAOLkssUYr9ckLGasz+iTYzpUjb166Ysph4rY+1idtuN3/TzbuL7yDCT0+lJFUjaUNenb23GRGbHwBWd0Gbg==} '@next/mdx@14.2.16': resolution: {integrity: sha512-IVd/Z3vYpIZ/nzqhmSHmTKfSrQ6eZrorkzs0uzMCXt3hL6lw4qf/jcEExZmXoenqnZ5vX0xkou+y4uWAKZhvfw==} @@ -3426,8 +3426,8 @@ packages: cpu: [arm64] os: [darwin] - '@next/swc-darwin-arm64@15.0.4-canary.5': - resolution: {integrity: sha512-9N6FZd6Cik7+08cT2GDON5l148wqvauw7u75iixqZ1Tz8BBHT70qCbDJGonWGM60PphgDNqBxCXGuJSTBjQHow==} + '@next/swc-darwin-arm64@15.0.4-canary.8': + resolution: {integrity: sha512-EC/ocgdRKIObddlM5Tr+Iw6xFauYQIlQCUwhsknKhOwZ3d7PlyS5yBHqZLHyQUrrvzw2qV1fUVJ8i0CGXACCFQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -3444,8 +3444,8 @@ packages: cpu: [x64] os: [darwin] - '@next/swc-darwin-x64@15.0.4-canary.5': - resolution: {integrity: sha512-Rox0ihBOKV1Er7AGZj+q/WhNemobEsXMTwrwyELza9VoAoOBH6XJXD+/KSCTXKcWLont+xiJK7gxsZkc+rYR+w==} + '@next/swc-darwin-x64@15.0.4-canary.8': + resolution: {integrity: sha512-xqEIPzw+a+nSG4oomNQErkwmpr4E2it/mR6EE2sUc+JR0DBVosXrP2KjVkvDKb15HpB3ZKP7b4ugOcfyNvfe1Q==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -3474,8 +3474,8 @@ packages: cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-gnu@15.0.4-canary.5': - resolution: {integrity: sha512-S+BjJl3lLEsiuMadXXHWgzL3A/RYXiv8Ngx2IbmWrCVIEGBfnXWJcbGFVN4PwPUiLK8Ymdg0mqgybOSUcHG4Ng==} + '@next/swc-linux-arm64-gnu@15.0.4-canary.8': + resolution: {integrity: sha512-BSdzXJCgaf/dzdeeQjQPEeOACqMqYkyAQrCvukpkqg7GKsABldTYCXU4u1FUOydx2jzpmnPPOigEfKfC9Dbnxw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -3492,8 +3492,8 @@ packages: cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.0.4-canary.5': - resolution: {integrity: sha512-OMt5j/zVueGy4vr2nYvsApjedt5vcDOnQ3V5s96jrpvNnSgPknmLYe5nZo7EXpnEnIWqNr7KqqwQuVvXoqUr1w==} + '@next/swc-linux-arm64-musl@15.0.4-canary.8': + resolution: {integrity: sha512-96NZEHxpJCryY+Qvuh08KbG2fNpjmL0/Ed+3Il/lfxhXuL4pLe2FARrvczK7aBI/Q1xoFZsPFu09xFux0qiryA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -3510,8 +3510,8 @@ packages: cpu: [x64] os: [linux] - '@next/swc-linux-x64-gnu@15.0.4-canary.5': - resolution: {integrity: sha512-/2wQvZHDYfL4xqp5ltl6eHLr3tlj587WwpVtqZjGMJ2grihGMb9CrK5MxUXqgzxUBTjMRR2OtRuJo9A6rPvm8Q==} + '@next/swc-linux-x64-gnu@15.0.4-canary.8': + resolution: {integrity: sha512-hanz4B+ZkqNvXBAWoFLOnwZG8e9kNDhR57ND+eW7PZzyRt8UvXjzwidDSzHF0M0kVDpdkaA2OgjT1sUEO/FCfg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -3528,8 +3528,8 @@ packages: cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.0.4-canary.5': - resolution: {integrity: sha512-dncDPb0cAnNHxBiI5Yon9zm3Aw83fZJybekQeA6b5P+YNR69IZnRYmpDUxgfwNPUyV5fh2kqEAAFH4ZmlQSmCw==} + '@next/swc-linux-x64-musl@15.0.4-canary.8': + resolution: {integrity: sha512-gUNxfwD2KVfW3R5B5Sgy06PEP04SfwTd6QD3p4XOfkwrjs6dCxOf00DO4GynZrNBp41Ry0+zYplyp7Y4pVBJUQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -3546,8 +3546,8 @@ packages: cpu: [arm64] os: [win32] - '@next/swc-win32-arm64-msvc@15.0.4-canary.5': - resolution: {integrity: sha512-WtTJ2PuzLPkq4PTWik6ZK+jdY2SDpkIXkRwnB9K501GS7f5wvl/OExO81r+StwlKnFjHBq5MSsPVNAJfIKjdnw==} + '@next/swc-win32-arm64-msvc@15.0.4-canary.8': + resolution: {integrity: sha512-odiKp1tqm9xNqct22B19ZbLMfDD3jSa2kEbitXL+cjtIo4g5lbiDSX6+3qoriLtLXC3YDPzy3U8kGlnp/5ZYdg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -3576,8 +3576,8 @@ packages: cpu: [x64] os: [win32] - '@next/swc-win32-x64-msvc@15.0.4-canary.5': - resolution: {integrity: sha512-QuaB5A2IQj9K+B2oTl61LYPISYan/x+biwnRD7DbyBEIDWkzJPjMpxKqP62Kkmv91GfhqCql3WlOWbqW+WemRg==} + '@next/swc-win32-x64-msvc@15.0.4-canary.8': + resolution: {integrity: sha512-TIs1/0zWx7dWjCYcwgQPiZ79Br1IzNwgklhLkLmGq1lboetNdhv1jY3Qc+Z/BE1FezCJJhx9kX8fX2kAjmOc2Q==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -10648,16 +10648,16 @@ packages: sass: optional: true - next@15.0.4-canary.5: - resolution: {integrity: sha512-0Xb0tjDadaH3KNonyZnT97+fGOet13WlVAGhTfGLNkz3BZ4GvD88vT7Qx2pkoPgPN0Cr9mxQrNNwpZ0sLbVxHg==} + next@15.0.4-canary.8: + resolution: {integrity: sha512-KhlwoPt/G558HldvJfCn5fIMSCI6PO7s9eVsUxg4wxVGV4HbBG+k9h9fhizw3Gba1WhhOIv7KAIQ27LQbFkybA==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 '@playwright/test': ^1.41.2 babel-plugin-react-compiler: '*' - react: ^18.2.0 || 19.0.0-rc-66855b96-20241106 - react-dom: ^18.2.0 || 19.0.0-rc-66855b96-20241106 + react: ^18.2.0 || 19.0.0-rc-5c56b873-20241107 + react-dom: ^18.2.0 || 19.0.0-rc-5c56b873-20241107 sass: ^1.3.0 peerDependenciesMeta: '@opentelemetry/api': @@ -17564,7 +17564,7 @@ snapshots: '@next/env@14.2.16': {} - '@next/env@15.0.4-canary.5': {} + '@next/env@15.0.4-canary.8': {} '@next/mdx@14.2.16(@mdx-js/loader@3.1.0(webpack@5.95.0(esbuild@0.23.1)))(@mdx-js/react@3.1.0(@types/react@18.3.12)(react@18.3.1))': dependencies: @@ -17585,7 +17585,7 @@ snapshots: '@next/swc-darwin-arm64@14.2.16': optional: true - '@next/swc-darwin-arm64@15.0.4-canary.5': + '@next/swc-darwin-arm64@15.0.4-canary.8': optional: true '@next/swc-darwin-x64@12.3.4': @@ -17594,7 +17594,7 @@ snapshots: '@next/swc-darwin-x64@14.2.16': optional: true - '@next/swc-darwin-x64@15.0.4-canary.5': + '@next/swc-darwin-x64@15.0.4-canary.8': optional: true '@next/swc-freebsd-x64@12.3.4': @@ -17609,7 +17609,7 @@ snapshots: '@next/swc-linux-arm64-gnu@14.2.16': optional: true - '@next/swc-linux-arm64-gnu@15.0.4-canary.5': + '@next/swc-linux-arm64-gnu@15.0.4-canary.8': optional: true '@next/swc-linux-arm64-musl@12.3.4': @@ -17618,7 +17618,7 @@ snapshots: '@next/swc-linux-arm64-musl@14.2.16': optional: true - '@next/swc-linux-arm64-musl@15.0.4-canary.5': + '@next/swc-linux-arm64-musl@15.0.4-canary.8': optional: true '@next/swc-linux-x64-gnu@12.3.4': @@ -17627,7 +17627,7 @@ snapshots: '@next/swc-linux-x64-gnu@14.2.16': optional: true - '@next/swc-linux-x64-gnu@15.0.4-canary.5': + '@next/swc-linux-x64-gnu@15.0.4-canary.8': optional: true '@next/swc-linux-x64-musl@12.3.4': @@ -17636,7 +17636,7 @@ snapshots: '@next/swc-linux-x64-musl@14.2.16': optional: true - '@next/swc-linux-x64-musl@15.0.4-canary.5': + '@next/swc-linux-x64-musl@15.0.4-canary.8': optional: true '@next/swc-win32-arm64-msvc@12.3.4': @@ -17645,7 +17645,7 @@ snapshots: '@next/swc-win32-arm64-msvc@14.2.16': optional: true - '@next/swc-win32-arm64-msvc@15.0.4-canary.5': + '@next/swc-win32-arm64-msvc@15.0.4-canary.8': optional: true '@next/swc-win32-ia32-msvc@12.3.4': @@ -17660,7 +17660,7 @@ snapshots: '@next/swc-win32-x64-msvc@14.2.16': optional: true - '@next/swc-win32-x64-msvc@15.0.4-canary.5': + '@next/swc-win32-x64-msvc@15.0.4-canary.8': optional: true '@nodelib/fs.scandir@2.1.5': @@ -19696,7 +19696,7 @@ snapshots: '@vanilla-extract/private@1.0.3': {} - '@vercel/analytics@1.3.1(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@vercel/analytics@1.3.1(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': dependencies: server-only: 0.0.1 optionalDependencies: @@ -19709,7 +19709,7 @@ snapshots: satori: 0.10.9 yoga-wasm-web: 0.3.3 - '@vercel/speed-insights@1.0.13(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@vercel/speed-insights@1.0.13(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': optionalDependencies: next: 14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -20385,6 +20385,16 @@ snapshots: atob@2.1.2: {} + autoprefixer@10.4.20(postcss@8.4.39): + dependencies: + browserslist: 4.24.0 + caniuse-lite: 1.0.30001669 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.0.1 + postcss: 8.4.39 + postcss-value-parser: 4.2.0 + autoprefixer@10.4.20(postcss@8.4.47): dependencies: browserslist: 4.24.0 @@ -27541,7 +27551,7 @@ snapshots: dependencies: type-fest: 2.19.0 - next-auth@4.24.8(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-auth@4.24.8(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.24.7 '@panva/hkdf': 1.2.0 @@ -27556,7 +27566,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) uuid: 8.3.2 - next-sitemap@4.2.3(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + next-sitemap@4.2.3(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: '@corex/deepmerge': 4.0.43 '@next/env': 13.5.6 @@ -27580,7 +27590,7 @@ snapshots: transitivePeerDependencies: - supports-color - next@12.3.4(@babel/core@7.25.9)(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + next@12.3.4(react-dom@17.0.2(react@17.0.2))(react@17.0.2): dependencies: '@next/env': 12.3.4 '@swc/helpers': 0.4.11 @@ -27588,7 +27598,7 @@ snapshots: postcss: 8.4.14 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - styled-jsx: 5.0.7(@babel/core@7.25.9)(react@17.0.2) + styled-jsx: 5.0.7(react@17.0.2) use-sync-external-store: 1.2.0(react@17.0.2) optionalDependencies: '@next/swc-android-arm-eabi': 12.3.4 @@ -27634,9 +27644,9 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@15.0.4-canary.5(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@15.0.4-canary.8(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@next/env': 15.0.4-canary.5 + '@next/env': 15.0.4-canary.8 '@swc/counter': 0.1.3 '@swc/helpers': 0.5.13 busboy: 1.6.0 @@ -27646,21 +27656,21 @@ snapshots: react-dom: 18.3.1(react@18.3.1) styled-jsx: 5.1.6(@babel/core@7.25.9)(react@18.3.1) optionalDependencies: - '@next/swc-darwin-arm64': 15.0.4-canary.5 - '@next/swc-darwin-x64': 15.0.4-canary.5 - '@next/swc-linux-arm64-gnu': 15.0.4-canary.5 - '@next/swc-linux-arm64-musl': 15.0.4-canary.5 - '@next/swc-linux-x64-gnu': 15.0.4-canary.5 - '@next/swc-linux-x64-musl': 15.0.4-canary.5 - '@next/swc-win32-arm64-msvc': 15.0.4-canary.5 - '@next/swc-win32-x64-msvc': 15.0.4-canary.5 + '@next/swc-darwin-arm64': 15.0.4-canary.8 + '@next/swc-darwin-x64': 15.0.4-canary.8 + '@next/swc-linux-arm64-gnu': 15.0.4-canary.8 + '@next/swc-linux-arm64-musl': 15.0.4-canary.8 + '@next/swc-linux-x64-gnu': 15.0.4-canary.8 + '@next/swc-linux-x64-musl': 15.0.4-canary.8 + '@next/swc-win32-arm64-msvc': 15.0.4-canary.8 + '@next/swc-win32-x64-msvc': 15.0.4-canary.8 '@playwright/test': 1.48.1 sharp: 0.33.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - nextra-theme-docs@3.1.0(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + nextra-theme-docs@3.1.0(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@headlessui/react': 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) clsx: 2.1.1 @@ -27668,13 +27678,13 @@ snapshots: flexsearch: 0.7.43 next: 14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - nextra: 3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + nextra: 3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) scroll-into-view-if-needed: 3.1.0 zod: 3.23.8 - nextra@3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@babel/core@7.25.9)(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3): + nextra@3.1.0(@types/react@18.3.12)(acorn@8.13.0)(next@14.2.16(@playwright/test@1.48.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3): dependencies: '@formatjs/intl-localematcher': 0.5.5 '@headlessui/react': 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -30752,11 +30762,9 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-jsx@5.0.7(@babel/core@7.25.9)(react@17.0.2): + styled-jsx@5.0.7(react@17.0.2): dependencies: react: 17.0.2 - optionalDependencies: - '@babel/core': 7.25.9 styled-jsx@5.1.1(@babel/core@7.25.9)(react@18.3.1): dependencies: From 2e8396746138fe1e0db6a3cac481164df4237e3e Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 13 Nov 2024 14:24:46 +0100 Subject: [PATCH 14/28] add suspense --- examples/example-app-router/src/components/BaseLayout.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/example-app-router/src/components/BaseLayout.tsx b/examples/example-app-router/src/components/BaseLayout.tsx index 8907d4e9b..61344cc18 100644 --- a/examples/example-app-router/src/components/BaseLayout.tsx +++ b/examples/example-app-router/src/components/BaseLayout.tsx @@ -2,7 +2,7 @@ import {clsx} from 'clsx'; import {Inter} from 'next/font/google'; import {NextIntlClientProvider} from 'next-intl'; import {getMessages} from 'next-intl/server'; -import {ReactNode} from 'react'; +import {ReactNode, Suspense} from 'react'; import Navigation from '@/components/Navigation'; const inter = Inter({subsets: ['latin']}); @@ -21,8 +21,10 @@ export default async function BaseLayout({children, locale}: Props) { - - {children} + + + {children} + From cf075fd21cdc86fa9d6c1dc74e1af645cf4c1657 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 13 Nov 2024 14:47:33 +0100 Subject: [PATCH 15/28] move suspense up a level --- examples/example-app-router/src/components/BaseLayout.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/example-app-router/src/components/BaseLayout.tsx b/examples/example-app-router/src/components/BaseLayout.tsx index 61344cc18..460aedbbc 100644 --- a/examples/example-app-router/src/components/BaseLayout.tsx +++ b/examples/example-app-router/src/components/BaseLayout.tsx @@ -20,12 +20,12 @@ export default async function BaseLayout({children, locale}: Props) { return ( - - + + {children} - - + + ); From 204b5dfe7464b7d83141d55e7455c824513fa574 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 13 Nov 2024 15:50:52 +0100 Subject: [PATCH 16/28] feat!: Don't read a default for `locale` from `useParams.locale` on the client side --- packages/next-intl/eslint.config.mjs | 16 ++++++- .../src/navigation/createNavigation.test.tsx | 28 +++++------- .../react-client/createNavigation.test.tsx | 36 +++------------ .../react-client/createNavigation.tsx | 3 +- .../react-client/useBasePathname.test.tsx | 17 +++---- .../react-client/useBasePathname.tsx | 2 +- .../src/navigation/shared/BaseLink.tsx | 3 +- packages/next-intl/src/react-client/index.tsx | 3 -- .../src/react-client/useLocale.test.tsx | 33 -------------- .../next-intl/src/react-client/useLocale.tsx | 23 ---------- .../shared/NextIntlClientProvider.test.tsx | 44 ------------------- .../src/shared/NextIntlClientProvider.tsx | 20 +++------ packages/next-intl/src/shared/constants.tsx | 3 -- packages/next-intl/src/shared/useParams.tsx | 7 --- 14 files changed, 50 insertions(+), 188 deletions(-) delete mode 100644 packages/next-intl/src/react-client/useLocale.test.tsx delete mode 100644 packages/next-intl/src/react-client/useLocale.tsx delete mode 100644 packages/next-intl/src/shared/NextIntlClientProvider.test.tsx delete mode 100644 packages/next-intl/src/shared/useParams.tsx diff --git a/packages/next-intl/eslint.config.mjs b/packages/next-intl/eslint.config.mjs index 92ba53b1f..52a0ca5f5 100644 --- a/packages/next-intl/eslint.config.mjs +++ b/packages/next-intl/eslint.config.mjs @@ -6,6 +6,20 @@ export default (await getPresets('typescript', 'react', 'vitest')).concat({ 'react-compiler': reactCompilerPlugin }, rules: { - 'react-compiler/react-compiler': 'error' + 'react-compiler/react-compiler': 'error', + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + // Two reasons: + // - Avoid hardcoding the `locale` param + // - Prepare for a new API in Next.js to read params deeply + name: 'next/navigation.js', + importNames: ['useParams'] + } + ] + } + ] } }); diff --git a/packages/next-intl/src/navigation/createNavigation.test.tsx b/packages/next-intl/src/navigation/createNavigation.test.tsx index 92f05556b..c8118d154 100644 --- a/packages/next-intl/src/navigation/createNavigation.test.tsx +++ b/packages/next-intl/src/navigation/createNavigation.test.tsx @@ -2,29 +2,27 @@ import {render, screen} from '@testing-library/react'; import { RedirectType, permanentRedirect as nextPermanentRedirect, - redirect as nextRedirect, - useParams as nextUseParams + redirect as nextRedirect } from 'next/navigation.js'; import {renderToString} from 'react-dom/server'; -import {Locale} from 'use-intl'; +import {Locale, useLocale} from 'use-intl'; import {beforeEach, describe, expect, it, vi} from 'vitest'; -import {useLocale} from '../index.react-server.tsx'; import {DomainsConfig, Pathnames, defineRouting} from '../routing.tsx'; import createNavigationClient from './react-client/createNavigation.tsx'; import createNavigationServer from './react-server/createNavigation.tsx'; import getServerLocale from './react-server/getServerLocale.tsx'; vi.mock('react'); -vi.mock('next/navigation.js', async () => { - const actual = await vi.importActual('next/navigation.js'); - return { - ...actual, - useParams: vi.fn(() => ({locale: 'en'})), - redirect: vi.fn(), - permanentRedirect: vi.fn() - }; -}); +vi.mock('next/navigation.js', async () => ({ + ...(await vi.importActual('next/navigation.js')), + redirect: vi.fn(), + permanentRedirect: vi.fn() +})); vi.mock('./react-server/getServerLocale'); +vi.mock('use-intl', async () => ({ + ...(await vi.importActual('use-intl')), + useLocale: vi.fn(() => 'en') +})); function mockCurrentLocale(locale: Locale) { // Enable synchronous rendering without having to suspend @@ -35,9 +33,7 @@ function mockCurrentLocale(locale: Locale) { vi.mocked(getServerLocale).mockImplementation(() => promise); - vi.mocked(nextUseParams<{locale: Locale}>).mockImplementation(() => ({ - locale - })); + vi.mocked(useLocale).mockImplementation(() => locale); } function mockLocation(location: Partial) { diff --git a/packages/next-intl/src/navigation/react-client/createNavigation.test.tsx b/packages/next-intl/src/navigation/react-client/createNavigation.test.tsx index ca9b0968b..ef57b97c7 100644 --- a/packages/next-intl/src/navigation/react-client/createNavigation.test.tsx +++ b/packages/next-intl/src/navigation/react-client/createNavigation.test.tsx @@ -1,21 +1,22 @@ import {fireEvent, render, screen} from '@testing-library/react'; import { usePathname as useNextPathname, - useRouter as useNextRouter, - useParams + useRouter as useNextRouter } from 'next/navigation.js'; import type {Locale} from 'use-intl'; +import {useLocale} from 'use-intl'; import {beforeEach, describe, expect, it, vi} from 'vitest'; -import {NextIntlClientProvider, useLocale} from '../../index.react-client.tsx'; import {DomainsConfig, Pathnames} from '../../routing.tsx'; import createNavigation from './createNavigation.tsx'; vi.mock('next/navigation.js'); +vi.mock('use-intl', async () => ({ + ...(await vi.importActual('use-intl')), + useLocale: vi.fn(() => 'en') +})); function mockCurrentLocale(locale: Locale) { - vi.mocked(useParams<{locale: Locale}>).mockImplementation(() => ({ - locale - })); + vi.mocked(useLocale).mockImplementation(() => locale); } function mockLocation( @@ -112,29 +113,6 @@ describe("localePrefix: 'always'", () => { }); describe('Link', () => { - describe('usage outside of Next.js', () => { - beforeEach(() => { - vi.mocked(useParams).mockImplementation((() => null) as any); - }); - - it('works with a provider', () => { - render( - - Test - - ); - expect( - screen.getByRole('link', {name: 'Test'}).getAttribute('href') - ).toBe('/en/test'); - }); - - it('throws without a provider', () => { - expect(() => render(Test)).toThrow( - 'No intl context found. Have you configured the provider?' - ); - }); - }); - it('can receive a ref', () => { let ref; diff --git a/packages/next-intl/src/navigation/react-client/createNavigation.tsx b/packages/next-intl/src/navigation/react-client/createNavigation.tsx index 0cdcc4a56..904eab475 100644 --- a/packages/next-intl/src/navigation/react-client/createNavigation.tsx +++ b/packages/next-intl/src/navigation/react-client/createNavigation.tsx @@ -3,8 +3,7 @@ import { useRouter as useNextRouter } from 'next/navigation.js'; import {useMemo} from 'react'; -import type {Locale} from 'use-intl'; -import useLocale from '../../react-client/useLocale.tsx'; +import {type Locale, useLocale} from 'use-intl'; import { RoutingConfigLocalizedNavigation, RoutingConfigSharedNavigation diff --git a/packages/next-intl/src/navigation/react-client/useBasePathname.test.tsx b/packages/next-intl/src/navigation/react-client/useBasePathname.test.tsx index 4d4ea8839..28fcfc738 100644 --- a/packages/next-intl/src/navigation/react-client/useBasePathname.test.tsx +++ b/packages/next-intl/src/navigation/react-client/useBasePathname.test.tsx @@ -1,14 +1,18 @@ import {render, screen} from '@testing-library/react'; -import {usePathname as useNextPathname, useParams} from 'next/navigation.js'; +import {usePathname as useNextPathname} from 'next/navigation.js'; import {beforeEach, describe, expect, it, vi} from 'vitest'; -import {NextIntlClientProvider} from '../../index.react-client.tsx'; +import {NextIntlClientProvider, useLocale} from '../../index.react-client.tsx'; import useBasePathname from './useBasePathname.tsx'; vi.mock('next/navigation.js'); +vi.mock('use-intl', async () => ({ + ...(await vi.importActual('use-intl')), + useLocale: vi.fn(() => 'en') +})); function mockPathname(pathname: string) { vi.mocked(useNextPathname).mockImplementation(() => pathname); - vi.mocked(useParams).mockImplementation(() => ({locale: 'en'})); + vi.mocked(useLocale).mockImplementation(() => 'en'); } function Component() { @@ -51,7 +55,6 @@ describe('prefixed routing', () => { describe('usage outside of Next.js', () => { beforeEach(() => { vi.mocked(useNextPathname).mockImplementation((() => null) as any); - vi.mocked(useParams).mockImplementation((() => null) as any); }); it('returns `null` when used within a provider', () => { @@ -62,10 +65,4 @@ describe('usage outside of Next.js', () => { ); expect(container.innerHTML).toBe(''); }); - - it('throws without a provider', () => { - expect(() => render()).toThrow( - 'No intl context found. Have you configured the provider?' - ); - }); }); diff --git a/packages/next-intl/src/navigation/react-client/useBasePathname.tsx b/packages/next-intl/src/navigation/react-client/useBasePathname.tsx index 9bca6df97..c5eaa3c7a 100644 --- a/packages/next-intl/src/navigation/react-client/useBasePathname.tsx +++ b/packages/next-intl/src/navigation/react-client/useBasePathname.tsx @@ -1,6 +1,6 @@ import {usePathname as useNextPathname} from 'next/navigation.js'; import {useMemo} from 'react'; -import useLocale from '../../react-client/useLocale.tsx'; +import {useLocale} from 'use-intl'; import { LocalePrefixConfigVerbose, LocalePrefixMode, diff --git a/packages/next-intl/src/navigation/shared/BaseLink.tsx b/packages/next-intl/src/navigation/shared/BaseLink.tsx index 2ae09aa87..bd531725e 100644 --- a/packages/next-intl/src/navigation/shared/BaseLink.tsx +++ b/packages/next-intl/src/navigation/shared/BaseLink.tsx @@ -10,8 +10,7 @@ import { useEffect, useState } from 'react'; -import type {Locale} from 'use-intl'; -import useLocale from '../../react-client/useLocale.tsx'; +import {type Locale, useLocale} from 'use-intl'; import {InitializedLocaleCookieConfig} from '../../routing/config.tsx'; import syncLocaleCookie from './syncLocaleCookie.tsx'; diff --git a/packages/next-intl/src/react-client/index.tsx b/packages/next-intl/src/react-client/index.tsx index 094b4f0b4..e8ac8b466 100644 --- a/packages/next-intl/src/react-client/index.tsx +++ b/packages/next-intl/src/react-client/index.tsx @@ -46,7 +46,4 @@ export const useFormatter = callHook( base_useFormatter ) as typeof base_useFormatter; -// Replace `useLocale` export from `use-intl` -export {default as useLocale} from './useLocale.tsx'; - export {default as NextIntlClientProvider} from '../shared/NextIntlClientProvider.tsx'; diff --git a/packages/next-intl/src/react-client/useLocale.test.tsx b/packages/next-intl/src/react-client/useLocale.test.tsx deleted file mode 100644 index 2062bd8fc..000000000 --- a/packages/next-intl/src/react-client/useLocale.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import {render, screen} from '@testing-library/react'; -import {useParams} from 'next/navigation.js'; -import {expect, it, vi} from 'vitest'; -import {NextIntlClientProvider, useLocale} from './index.tsx'; - -vi.mock('next/navigation.js', () => ({ - useParams: vi.fn(() => ({locale: 'en'})) -})); - -function Component() { - return <>{useLocale()}; -} - -it('returns a locale from `useParams` without a provider', () => { - render(); - screen.getByText('en'); -}); - -it('prioritizes the locale from the provider', () => { - render( - - - - ); - screen.getByText('de'); -}); - -it('throws if neither a locale from the provider or useParams is available', () => { - vi.mocked(useParams).mockImplementation(() => ({})); - expect(() => render()).toThrow( - 'No intl context found. Have you configured the provider?' - ); -}); diff --git a/packages/next-intl/src/react-client/useLocale.tsx b/packages/next-intl/src/react-client/useLocale.tsx deleted file mode 100644 index 7274813d9..000000000 --- a/packages/next-intl/src/react-client/useLocale.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import {useLocale as useBaseLocale} from 'use-intl/react'; -import {LOCALE_SEGMENT_NAME} from '../shared/constants.tsx'; -import useParams from '../shared/useParams.tsx'; - -export default function useLocale(): ReturnType { - const params = useParams(); - - let locale; - - try { - // eslint-disable-next-line react-compiler/react-compiler - // eslint-disable-next-line react-hooks/rules-of-hooks, react-compiler/react-compiler -- False positive - locale = useBaseLocale(); - } catch (error) { - if (typeof params?.[LOCALE_SEGMENT_NAME] === 'string') { - locale = params[LOCALE_SEGMENT_NAME]; - } else { - throw error; - } - } - - return locale; -} diff --git a/packages/next-intl/src/shared/NextIntlClientProvider.test.tsx b/packages/next-intl/src/shared/NextIntlClientProvider.test.tsx deleted file mode 100644 index 7cf3b13c8..000000000 --- a/packages/next-intl/src/shared/NextIntlClientProvider.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import {render, screen} from '@testing-library/react'; -import type {Locale} from 'use-intl'; -import {it, vi} from 'vitest'; -import { - NextIntlClientProvider, - useTranslations -} from '../index.react-client.tsx'; - -vi.mock('next/navigation.js', () => ({ - useParams() { - return {locale: 'en'}; - } -})); - -function Component() { - const t = useTranslations(); - return <>{t('message', {price: 29000.5})}; -} - -function TestProvider({locale}: {locale?: Locale}) { - return ( - - - - ); -} - -it('can use messages from the provider', () => { - render(); - screen.getByText('€29,000.50'); -}); - -it('reads a default locale from params', () => { - render(); - screen.getByText('€29,000.50'); -}); - -it('can override the locale from Next.js', () => { - render(); - screen.getByText('29.000,50 €'); -}); diff --git a/packages/next-intl/src/shared/NextIntlClientProvider.tsx b/packages/next-intl/src/shared/NextIntlClientProvider.tsx index dc18bf15d..d079dd428 100644 --- a/packages/next-intl/src/shared/NextIntlClientProvider.tsx +++ b/packages/next-intl/src/shared/NextIntlClientProvider.tsx @@ -1,10 +1,8 @@ 'use client'; import {ComponentProps} from 'react'; -import type {Locale} from 'use-intl'; +import {type Locale} from 'use-intl'; import {IntlProvider} from 'use-intl/react'; -import {LOCALE_SEGMENT_NAME} from './constants.tsx'; -import useParams from './useParams.tsx'; type Props = Omit, 'locale'> & { /** This is automatically received when being rendered from a Server Component. In all other cases, e.g. when rendered from a Client Component, a unit test or with the Pages Router, you can pass this prop explicitly. */ @@ -12,18 +10,12 @@ type Props = Omit, 'locale'> & { }; export default function NextIntlClientProvider({locale, ...rest}: Props) { - const paramsLocale = useParams()?.[LOCALE_SEGMENT_NAME]; - if (!locale) { - if (typeof paramsLocale === 'string') { - locale = paramsLocale; - } else { - throw new Error( - process.env.NODE_ENV !== 'production' - ? 'Failed to determine locale in `NextIntlClientProvider`, please provide the `locale` prop explicitly.\n\nSee https://next-intl-docs.vercel.app/docs/configuration#locale' - : undefined - ); - } + throw new Error( + process.env.NODE_ENV !== 'production' + ? "Couldn't infer the `locale` prop in `NextIntlClientProvider`, please provide it explicitly.\n\nSee https://next-intl-docs.vercel.app/docs/configuration#locale" + : undefined + ); } return ; diff --git a/packages/next-intl/src/shared/constants.tsx b/packages/next-intl/src/shared/constants.tsx index ac19219d6..d86ae9878 100644 --- a/packages/next-intl/src/shared/constants.tsx +++ b/packages/next-intl/src/shared/constants.tsx @@ -1,5 +1,2 @@ // Used to read the locale from the middleware export const HEADER_LOCALE_NAME = 'X-NEXT-INTL-LOCALE'; - -// In a URL like "/en-US/about", the locale segment is "en-US" -export const LOCALE_SEGMENT_NAME = 'locale'; diff --git a/packages/next-intl/src/shared/useParams.tsx b/packages/next-intl/src/shared/useParams.tsx deleted file mode 100644 index 7aed6c35a..000000000 --- a/packages/next-intl/src/shared/useParams.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import {useParams as useNextParams} from 'next/navigation.js'; - -export default function useParams() { - // The types aren't entirely correct here. Outside of Next.js - // `useParams` can be called, but the return type is `null`. - return useNextParams() as ReturnType | null; -} From 4b678cd5d5d48f15f92e1dab98588d82a2c26854 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 13 Nov 2024 16:34:56 +0100 Subject: [PATCH 17/28] adapt docs --- docs/src/pages/docs/usage/configuration.mdx | 74 ++++++++++----------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/docs/src/pages/docs/usage/configuration.mdx b/docs/src/pages/docs/usage/configuration.mdx index 4ded13d4b..093339f16 100644 --- a/docs/src/pages/docs/usage/configuration.mdx +++ b/docs/src/pages/docs/usage/configuration.mdx @@ -181,6 +181,17 @@ export default getRequestConfig(async () => { }); ``` +
+Which values can the `requestLocale` parameter hold? + +While the `requestLocale` parameter typically corresponds to the `[locale]` segment that was matched by the middleware, there are three special cases to consider: + +1. **Overrides**: When an explicit `locale` is passed to [awaitable functions](/docs/environments/actions-metadata-route-handlers) like `getTranslations({locale: 'en'})`, then this value will be used instead of the segment. +1. **`undefined`**: The value can be `undefined` when a page outside of the `[locale]` segment renders (e.g. a language selection page at `app/page.tsx`). +1. **Invalid values**: Since the `[locale]` segment effectively acts like a catch-all for unknown routes (e.g. `/unknown.txt`), invalid values should be replaced with a valid locale. In addition to this, you might want to call `notFound()` in [the root layout](/docs/getting-started/app-router/with-i18n-routing#layout) to abort the render in this case. + +
+ @@ -191,14 +202,13 @@ export default getRequestConfig(async () => { -
-Which values can the `requestLocale` parameter hold? +
+How can I change the locale? -While the `requestLocale` parameter typically corresponds to the `[locale]` segment that was matched by the middleware, there are three special cases to consider: +Depending on if you're using [i18n routing](/docs/getting-started/app-router), the locale can be changed as follows: -1. **Overrides**: When an explicit `locale` is passed to [awaitable functions](/docs/environments/actions-metadata-route-handlers) like `getTranslations({locale: 'en'})`, then this value will be used instead of the segment. -1. **`undefined`**: The value can be `undefined` when a page outside of the `[locale]` segment renders (e.g. a language selection page at `app/page.tsx`). -1. **Invalid values**: Since the `[locale]` segment effectively acts like a catch-all for unknown routes (e.g. `/unknown.txt`), invalid values should be replaced with a valid locale. In addition to this, you might want to call `notFound()` in [the root layout](/docs/getting-started/app-router/with-i18n-routing#layout) to abort the render in this case. +1. **With i18n routing**: The locale is managed by the router and can be changed by using navigation APIs from `next-intl` like [`Link`](/docs/routing/navigation#link) or [`useRouter`](/docs/routing/navigation#userouter). +2. **Without i18n routing**: You can change the locale by updating the value where the locale is read from (e.g. a cookie, a user setting, etc.). If you're looking for inspiration, you can have a look at the [App Router without i18n routing example](/examples#app-router-without-i18n-routing) that manages the locale via a cookie.
@@ -218,41 +228,13 @@ import {getLocale} from 'next-intl/server'; const locale = await getLocale(); ``` -### `Locale` type [#locale-type] - -When passing a `locale` to another function, you can use the `Locale` type for the receiving parameter: - -```tsx -import {Locale} from 'next-intl'; - -async function getPosts(locale: Locale) { - // ... -} -``` - - - By default, `Locale` is typed as `string`. However, you can optionally provide - a strict union based on your supported locales for this type by [augmenting - the `Locale` type](/docs/workflows/typescript#locale). - - -
-How can I change the locale? - -Depending on if you're using [i18n routing](/docs/getting-started/app-router), the locale can be changed as follows: - -1. **With i18n routing**: The locale is managed by the router and can be changed by using navigation APIs from `next-intl` like [`Link`](/docs/routing/navigation#link) or [`useRouter`](/docs/routing/navigation#userouter). -2. **Without i18n routing**: You can change the locale by updating the value where the locale is read from (e.g. a cookie, a user setting, etc.). If you're looking for inspiration, you can have a look at the [App Router without i18n routing example](/examples#app-router-without-i18n-routing) that manages the locale via a cookie. - -
-
Which value is returned from `useLocale`? -The returned value is resolved based on these priorities: +Depending on how a component renders, the returned locale corresponds to: -1. **Server Components**: If you're using [i18n routing](/docs/getting-started/app-router), the returned locale is the one that you've either provided via [`setRequestLocale`](/docs/getting-started/app-router/with-i18n-routing#static-rendering) or alternatively the one in the `[locale]` segment that was matched by the middleware. If you're not using i18n routing, the returned locale is the one that you've provided via `getRequestConfig`. -2. **Client Components**: In this case, the locale is received from `NextIntlClientProvider` or alternatively `useParams().locale`. Note that `NextIntlClientProvider` automatically inherits the locale if the component is rendered by a Server Component. +1. **Server Components**: The locale represents the value returned in [`i18n/request.ts`](#i18n-request). +2. **Client Components**: In this case, the locale is received from [`NextIntlClientProvider`](#nextintlclientprovider). Note that `NextIntlClientProvider` automatically inherits the locale if it is rendered by a Server Component.
@@ -277,6 +259,24 @@ return (
+### `Locale` type [#locale-type] + +When passing a `locale` to another function, you can use the `Locale` type for the receiving parameter: + +```tsx +import {Locale} from 'next-intl'; + +async function getPosts(locale: Locale) { + // ... +} +``` + + + By default, `Locale` is typed as `string`. However, you can optionally provide + a strict union based on your supported locales for this type by [augmenting + the `Locale` type](/docs/workflows/typescript#locale). + + ## Messages The most crucial aspect of internationalization is providing labels based on the user's language. The recommended workflow is to store your messages in your repository along with the code. From 17a9602efa55d0ca084c15ebc45f85d198056cb7 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 13 Nov 2024 18:01:02 +0100 Subject: [PATCH 18/28] fix playground, minor docs fixes --- .../pages/docs/environments/server-client-components.mdx | 5 ++++- .../src/app/[locale]/layout.tsx | 8 +++++--- packages/use-intl/src/react/useIntlContext.tsx | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/src/pages/docs/environments/server-client-components.mdx b/docs/src/pages/docs/environments/server-client-components.mdx index cccba84e4..b7c743ef4 100644 --- a/docs/src/pages/docs/environments/server-client-components.mdx +++ b/docs/src/pages/docs/environments/server-client-components.mdx @@ -69,7 +69,7 @@ These functions are available: 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. -In Next.js, Server Components are the default, and therefore shared components will typically execute as Server Components. +In Next.js, Server Components are the default, and therefore shared components will typically execute as Server Components: ```tsx filename="UserDetails.tsx" import {useTranslations} from 'next-intl'; @@ -77,6 +77,9 @@ import {useTranslations} from 'next-intl'; export default function UserDetails({user}) { const t = useTranslations('UserProfile'); + // This component will execute as a Server Component by default. + // However, if it is imported from a Client Component, it will + // execute as a Client Component. return (

{t('title')}

diff --git a/examples/example-app-router-playground/src/app/[locale]/layout.tsx b/examples/example-app-router-playground/src/app/[locale]/layout.tsx index d709cf78f..2a9b487b4 100644 --- a/examples/example-app-router-playground/src/app/[locale]/layout.tsx +++ b/examples/example-app-router-playground/src/app/[locale]/layout.tsx @@ -1,6 +1,6 @@ import {Metadata} from 'next'; import {notFound} from 'next/navigation'; -import {Locale, hasLocale} from 'next-intl'; +import {Locale, NextIntlClientProvider, hasLocale} from 'next-intl'; import { getFormatter, getNow, @@ -50,8 +50,10 @@ export default function LocaleLayout({children, params: {locale}}: Props) { lineHeight: 1.5 }} > - - {children} + + + {children} + diff --git a/packages/use-intl/src/react/useIntlContext.tsx b/packages/use-intl/src/react/useIntlContext.tsx index 9185ecf26..aa1312800 100644 --- a/packages/use-intl/src/react/useIntlContext.tsx +++ b/packages/use-intl/src/react/useIntlContext.tsx @@ -7,7 +7,7 @@ export default function useIntlContext(): IntlContextValue { if (!context) { throw new Error( process.env.NODE_ENV !== 'production' - ? 'No intl context found. Have you configured the provider? See https://next-intl-docs.vercel.app/docs/usage/configuration#client-server-components' + ? 'No intl context found. Have you configured the provider? See https://next-intl-docs.vercel.app/docs/usage/configuration#server-client-components' : undefined ); } From 1e66d0717d823d184f41756399518c281156a1df Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Thu, 14 Nov 2024 08:43:25 +0100 Subject: [PATCH 19/28] Lazy read `now` only when necessary in `createFormatter` --- docs/src/pages/docs/usage/configuration.mdx | 26 +++++++++-- docs/src/pages/docs/usage/dates-times.mdx | 30 ++++++------- .../src/react-server/useFormatter.test.tsx | 43 +++++++++++++++++++ .../src/react-server/useFormatter.tsx | 16 +------ .../src/react-server/useTranslations.test.tsx | 14 ++++++ .../src/react-server/useTranslations.tsx | 4 +- .../src/server/react-server/getDefaultNow.tsx | 6 ++- .../server/react-server/getFormatter.test.tsx | 35 +++++++++++++++ .../src/server/react-server/getFormatter.tsx | 3 +- .../react-server/getServerFormatter.tsx | 21 +++++++++ ...Translator.tsx => getServerTranslator.tsx} | 4 +- .../server/react-server/getTranslations.tsx | 4 +- .../use-intl/src/core/createFormatter.tsx | 25 ++++++----- 13 files changed, 177 insertions(+), 54 deletions(-) create mode 100644 packages/next-intl/src/react-server/useFormatter.test.tsx create mode 100644 packages/next-intl/src/server/react-server/getFormatter.test.tsx create mode 100644 packages/next-intl/src/server/react-server/getServerFormatter.tsx rename packages/next-intl/src/server/react-server/{getTranslator.tsx => getServerTranslator.tsx} (83%) diff --git a/docs/src/pages/docs/usage/configuration.mdx b/docs/src/pages/docs/usage/configuration.mdx index 4ded13d4b..313d1fa37 100644 --- a/docs/src/pages/docs/usage/configuration.mdx +++ b/docs/src/pages/docs/usage/configuration.mdx @@ -451,9 +451,7 @@ const timeZone = await getTimeZone(); ## Now value [#now] -When formatting [relative dates and times](/docs/usage/dates-times#relative-times), `next-intl` will format times in relation to a reference point in time that is referred to as "now". By default, this is the time a component renders. - -If you prefer to provide a global default, you can configure an explicit value for `now`: +When formatting [relative dates and times](/docs/usage/dates-times#relative-times), `next-intl` will format times in relation to a reference point in time that is referred to as "now". If you want to ensure that this value is consistent across components, you can configure a global `now` value: @@ -462,9 +460,11 @@ If you prefer to provide a global default, you can configure an explicit value f import {getRequestConfig} from 'next-intl/server'; async function now() { - 'use cache'; + // (if you're using `dynamicIO`) + // 'use cache'; // Use this value consistently + // when formatting relative times return new Date(); } @@ -479,6 +479,24 @@ export default getRequestConfig(async () => { If a `now` value is provided in `i18n/request.ts`, this will automatically be inherited by Client Components if you wrap them in a `NextIntlClientProvider` that is rendered by a Server Component. +
+How does the usage of `'use cache'` with `now` relate to cache expiration? + +If you're using [`dynamicIO`](https://nextjs.org/docs/canary/app/api-reference/config/next-config-js/dynamicIO), you can cache the value of `now` with the [`'use cache'`](https://nextjs.org/docs/canary/app/api-reference/directives/use-cache) directive: + +```tsx +async function now() { + 'use cache'; + const now = new Date(); +} +``` + +Since the request config from `i18n/request.ts` is shared among all Server Components that use features from `next-intl`, the cache expiration for `now` will apply to all of these, regardless of if they're using relative time formatting or not. + +If you want more granular cache control, you can consider passing the `now` value to [`format.relativeTime`](/docs/usage/dates-times#relative-times) explicitly as a second argument where relevant. + +
+
diff --git a/docs/src/pages/docs/usage/dates-times.mdx b/docs/src/pages/docs/usage/dates-times.mdx index b20409940..309a4f100 100644 --- a/docs/src/pages/docs/usage/dates-times.mdx +++ b/docs/src/pages/docs/usage/dates-times.mdx @@ -67,32 +67,30 @@ function Component() { const format = useFormatter(); const dateTime = new Date('2020-11-20T08:30:00.000Z'); - // At 2020-11-20T10:36:00.000Z, - // this will render "2 hours ago" - format.relativeTime(dateTime); + // A reference point in time + const now = new Date('2020-11-20T10:36:00.000Z'); + + // This will render "2 hours ago" + format.relativeTime(dateTime, now); } ``` Note that values are rounded, so e.g. if 126 minutes have passed, "2 hours ago" will be returned. -### Supplying `now` +### Providing `now` [#relative-times-now] -By default, `relativeTime` will use the [global value for `now`](/docs/usage/configuration#now). If you want to use a different value, you can explicitly pass this as the second parameter. +The `now` value can either be provided on a case-by-case basis or configured [globally](/docs/usage/configuration#now). -```js -import {useFormatter} from 'next-intl'; - -function Component() { - const format = useFormatter(); - const dateTime = new Date('2020-11-20T08:30:00.000Z'); - const now = new Date('2020-11-20T10:36:00.000Z'); +If you've configured a global `now` value, you can omit the corresponding parameter: - // Renders "2 hours ago" - format.relativeTime(dateTime, now); -} +```js +// Uses the global now value +format.relativeTime(dateTime); ``` -In case you want the relative time value to update over time, you can do so with [the `useNow` hook](/docs/usage/configuration#now): +### Continuously updating relative times [#relative-times-update] + +In case you want a relative time value to update over time, you can do so with [the `useNow` hook](/docs/usage/configuration#now): ```js import {useNow, useFormatter} from 'next-intl'; diff --git a/packages/next-intl/src/react-server/useFormatter.test.tsx b/packages/next-intl/src/react-server/useFormatter.test.tsx new file mode 100644 index 000000000..e6b5a3bee --- /dev/null +++ b/packages/next-intl/src/react-server/useFormatter.test.tsx @@ -0,0 +1,43 @@ +import {describe, expect, it, vi} from 'vitest'; +import getDefaultNow from '../server/react-server/getDefaultNow.tsx'; +import {renderToStream} from './testUtils.tsx'; +import useFormatter from './useFormatter.tsx'; + +vi.mock('react'); +vi.mock('../server/react-server/getDefaultNow.tsx', () => ({ + default: vi.fn(() => new Date()) +})); + +vi.mock('../../src/server/react-server/createRequestConfig', () => ({ + default: async () => ({ + locale: 'en' + }) +})); + +describe('dynamicIO', () => { + it('should not include `now` in the translator config', async () => { + function TestComponent() { + const format = useFormatter(); + format.dateTime(new Date()); + format.number(1); + format.dateTimeRange(new Date(), new Date()); + format.list(['a', 'b']); + format.relativeTime(new Date(), new Date()); + return null; + } + + await renderToStream(); + expect(getDefaultNow).not.toHaveBeenCalled(); + }); + + it('should read `now` for `relativeTime` if relying on a global `now`', async () => { + function TestComponent() { + const format = useFormatter(); + format.relativeTime(new Date()); + return null; + } + + await renderToStream(); + expect(getDefaultNow).toHaveBeenCalled(); + }); +}); diff --git a/packages/next-intl/src/react-server/useFormatter.tsx b/packages/next-intl/src/react-server/useFormatter.tsx index 567d6d12d..7e41b86b6 100644 --- a/packages/next-intl/src/react-server/useFormatter.tsx +++ b/packages/next-intl/src/react-server/useFormatter.tsx @@ -1,20 +1,8 @@ -import {cache} from 'react'; import type {useFormatter as useFormatterType} from 'use-intl'; -import {createFormatter} from 'use-intl/core'; -import getDefaultNow from '../server/react-server/getDefaultNow.tsx'; +import getServerFormatter from '../server/react-server/getServerFormatter.tsx'; import useConfig from './useConfig.tsx'; -const createFormatterCached = cache(createFormatter); - export default function useFormatter(): ReturnType { const config = useConfig('useFormatter'); - - return createFormatterCached({ - ...config, - // Only init when necessary to avoid triggering a `dynamicIO` error - // unnecessarily (`now` is only needed for `format.relativeTime`) - get now() { - return config.now ?? getDefaultNow(); - } - }); + return getServerFormatter(config); } diff --git a/packages/next-intl/src/react-server/useTranslations.test.tsx b/packages/next-intl/src/react-server/useTranslations.test.tsx index 1953d0ad5..6631d3943 100644 --- a/packages/next-intl/src/react-server/useTranslations.test.tsx +++ b/packages/next-intl/src/react-server/useTranslations.test.tsx @@ -31,6 +31,20 @@ vi.mock('use-intl/core', async (importActual) => { }; }); +describe('dynamicIO', () => { + it('should not include `now` in the translator config', async () => { + function TestComponent() { + useTranslations('A'); + return null; + } + + await renderToStream(); + expect(createTranslator).toHaveBeenCalledWith( + expect.not.objectContaining({now: expect.anything()}) + ); + }); +}); + describe('performance', () => { let attemptedRenders: Record; let finishedRenders: Record; diff --git a/packages/next-intl/src/react-server/useTranslations.tsx b/packages/next-intl/src/react-server/useTranslations.tsx index a8442631d..836b415bf 100644 --- a/packages/next-intl/src/react-server/useTranslations.tsx +++ b/packages/next-intl/src/react-server/useTranslations.tsx @@ -1,10 +1,10 @@ import type {useTranslations as useTranslationsType} from 'use-intl'; -import getTranslator from '../server/react-server/getTranslator.tsx'; +import getServerTranslator from '../server/react-server/getServerTranslator.tsx'; import useConfig from './useConfig.tsx'; export default function useTranslations( ...[namespace]: Parameters ): ReturnType { const config = useConfig('useTranslations'); - return getTranslator(config, namespace); + return getServerTranslator(config, namespace); } diff --git a/packages/next-intl/src/server/react-server/getDefaultNow.tsx b/packages/next-intl/src/server/react-server/getDefaultNow.tsx index 6d8ee5e14..fe296e494 100644 --- a/packages/next-intl/src/server/react-server/getDefaultNow.tsx +++ b/packages/next-intl/src/server/react-server/getDefaultNow.tsx @@ -1,8 +1,10 @@ import {cache} from 'react'; -function getDefaultNowImpl() { +function defaultNow() { + // See https://next-intl-docs.vercel.app/docs/usage/dates-times#relative-times return new Date(); } -const getDefaultNow = cache(getDefaultNowImpl); + +const getDefaultNow = cache(defaultNow); export default getDefaultNow; diff --git a/packages/next-intl/src/server/react-server/getFormatter.test.tsx b/packages/next-intl/src/server/react-server/getFormatter.test.tsx new file mode 100644 index 000000000..2499706da --- /dev/null +++ b/packages/next-intl/src/server/react-server/getFormatter.test.tsx @@ -0,0 +1,35 @@ +import {describe, expect, it, vi} from 'vitest'; +import getDefaultNow from './getDefaultNow.tsx'; +import getFormatter from './getFormatter.tsx'; + +vi.mock('react'); +vi.mock('./getDefaultNow.tsx', () => ({ + default: vi.fn(() => new Date()) +})); + +vi.mock('next-intl/config', () => ({ + default: async () => + ( + (await vi.importActual('../../../src/server/react-server')) as any + ).getRequestConfig({ + locale: 'en' + }) +})); + +describe('dynamicIO', () => { + it('should not read `now` unnecessarily', async () => { + const format = await getFormatter(); + format.dateTime(new Date()); + format.number(1); + format.dateTimeRange(new Date(), new Date()); + format.list(['a', 'b']); + format.relativeTime(new Date(), new Date()); + expect(getDefaultNow).not.toHaveBeenCalled(); + }); + + it('should read `now` for `relativeTime` if relying on a global `now`', async () => { + const format = await getFormatter(); + format.relativeTime(new Date()); + expect(getDefaultNow).toHaveBeenCalled(); + }); +}); diff --git a/packages/next-intl/src/server/react-server/getFormatter.tsx b/packages/next-intl/src/server/react-server/getFormatter.tsx index 9e1909076..1ba57e878 100644 --- a/packages/next-intl/src/server/react-server/getFormatter.tsx +++ b/packages/next-intl/src/server/react-server/getFormatter.tsx @@ -1,10 +1,11 @@ import {cache} from 'react'; import {type Locale, createFormatter} from 'use-intl/core'; import getConfig from './getConfig.tsx'; +import getServerFormatter from './getServerFormatter.tsx'; async function getFormatterCachedImpl(locale?: Locale) { const config = await getConfig(locale); - return createFormatter(config); + return getServerFormatter(config); } const getFormatterCached = cache(getFormatterCachedImpl); diff --git a/packages/next-intl/src/server/react-server/getServerFormatter.tsx b/packages/next-intl/src/server/react-server/getServerFormatter.tsx new file mode 100644 index 000000000..5beeb0820 --- /dev/null +++ b/packages/next-intl/src/server/react-server/getServerFormatter.tsx @@ -0,0 +1,21 @@ +import {cache} from 'react'; +import {createFormatter} from 'use-intl/core'; +import getDefaultNow from './getDefaultNow.tsx'; + +function getFormatterCachedImpl(config: Parameters[0]) { + // same here? + // also add a test + // also for getTranslations/useTranslations + // add a test with a getter maybe, don't mock + return createFormatter({ + ...config, + // Only init when necessary to avoid triggering a `dynamicIO` error + // unnecessarily (`now` is only needed for `format.relativeTime`) + get now() { + return config.now ?? getDefaultNow(); + } + }); +} +const getFormatterCached = cache(getFormatterCachedImpl); + +export default getFormatterCached; diff --git a/packages/next-intl/src/server/react-server/getTranslator.tsx b/packages/next-intl/src/server/react-server/getServerTranslator.tsx similarity index 83% rename from packages/next-intl/src/server/react-server/getTranslator.tsx rename to packages/next-intl/src/server/react-server/getServerTranslator.tsx index 3b4d7f50c..aec9d4a38 100644 --- a/packages/next-intl/src/server/react-server/getTranslator.tsx +++ b/packages/next-intl/src/server/react-server/getServerTranslator.tsx @@ -6,7 +6,7 @@ import { createTranslator } from 'use-intl/core'; -function getTranslatorImpl< +function getServerTranslatorImpl< NestedKey extends NamespaceKeys> = never >( config: Parameters[0], @@ -18,4 +18,4 @@ function getTranslatorImpl< }); } -export default cache(getTranslatorImpl); +export default cache(getServerTranslatorImpl); diff --git a/packages/next-intl/src/server/react-server/getTranslations.tsx b/packages/next-intl/src/server/react-server/getTranslations.tsx index 9b4047bf2..5615c2e46 100644 --- a/packages/next-intl/src/server/react-server/getTranslations.tsx +++ b/packages/next-intl/src/server/react-server/getTranslations.tsx @@ -7,7 +7,7 @@ import { createTranslator } from 'use-intl/core'; import getConfig from './getConfig.tsx'; -import getTranslator from './getTranslator.tsx'; +import getServerTranslator from './getServerTranslator.tsx'; // Maintainer note: `getTranslations` has two different call signatures. // We need to define these with function overloads, otherwise TypeScript @@ -41,7 +41,7 @@ async function getTranslations< } const config = await getConfig(locale); - return getTranslator(config, namespace); + return getServerTranslator(config, namespace); } export default cache(getTranslations); diff --git a/packages/use-intl/src/core/createFormatter.tsx b/packages/use-intl/src/core/createFormatter.tsx index 794473bf6..0ba242ce8 100644 --- a/packages/use-intl/src/core/createFormatter.tsx +++ b/packages/use-intl/src/core/createFormatter.tsx @@ -82,15 +82,16 @@ type Props = { _cache?: IntlCache; }; -export default function createFormatter({ - _cache: cache = createCache(), - _formatters: formatters = createIntlFormatters(cache), - formats, - locale, - now: globalNow, - onError = defaultOnError, - timeZone: globalTimeZone -}: Props) { +export default function createFormatter(props: Props) { + const { + _cache: cache = createCache(), + _formatters: formatters = createIntlFormatters(cache), + formats, + locale, + onError = defaultOnError, + timeZone: globalTimeZone + } = props; + function applyTimeZone(options?: DateTimeFormatOptions) { if (!options?.timeZone) { if (globalTimeZone) { @@ -212,8 +213,10 @@ export default function createFormatter({ } function getGlobalNow() { - if (globalNow) { - return globalNow; + // Only read when necessary to avoid triggering a `dynamicIO` error + // unnecessarily (`now` is only needed for `format.relativeTime`) + if (props.now) { + return props.now; } else { onError( new IntlError( From b4701848552c9bbe2379090424aad97c8d3309ff Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Thu, 14 Nov 2024 09:29:01 +0100 Subject: [PATCH 20/28] bump size --- packages/use-intl/.size-limit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/use-intl/.size-limit.ts b/packages/use-intl/.size-limit.ts index c6dae5c70..4267cf64d 100644 --- a/packages/use-intl/.size-limit.ts +++ b/packages/use-intl/.size-limit.ts @@ -5,7 +5,7 @@ const config: SizeLimitConfig = [ name: "import * from 'use-intl' (production)", import: '*', path: 'dist/esm/production/index.js', - limit: '12.945 kB' + limit: '12.965 kB' }, { name: "import {IntlProvider, useLocale, useNow, useTimeZone, useMessages, useFormatter} from 'use-intl' (production)", From 6063dd2178e023e34c432862462fb75c677eb016 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Thu, 14 Nov 2024 10:11:23 +0100 Subject: [PATCH 21/28] fix race condition for linting example-app-router-playground --- examples/example-app-router-playground/next.config.mjs | 5 ++++- turbo.json | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/example-app-router-playground/next.config.mjs b/examples/example-app-router-playground/next.config.mjs index eb3878299..0db82fe39 100644 --- a/examples/example-app-router-playground/next.config.mjs +++ b/examples/example-app-router-playground/next.config.mjs @@ -12,7 +12,10 @@ const withNextIntl = createNextIntlPlugin({ const withMdx = mdxPlugin(); export default withMdx( - withNextIntl({ + withNextIntl({, + eslint: { + ignoreDuringBuilds: true + }, trailingSlash: process.env.NEXT_PUBLIC_USE_CASE === 'trailing-slash', basePath: process.env.NEXT_PUBLIC_USE_CASE === 'base-path' diff --git a/turbo.json b/turbo.json index 49eabfe80..c1c687086 100644 --- a/turbo.json +++ b/turbo.json @@ -8,6 +8,9 @@ "lint": { "dependsOn": ["^build"] }, + "example-app-router-playground#lint": { + "dependsOn": ["example-app-router-playground#build"] + }, "test": { "dependsOn": ["build"] }, From 846fae8a4cda7c3e868bad5a4d81fd221893dfe5 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Thu, 14 Nov 2024 10:13:50 +0100 Subject: [PATCH 22/28] fix syntax error --- examples/example-app-router-playground/next.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/example-app-router-playground/next.config.mjs b/examples/example-app-router-playground/next.config.mjs index 0db82fe39..8c0332c69 100644 --- a/examples/example-app-router-playground/next.config.mjs +++ b/examples/example-app-router-playground/next.config.mjs @@ -12,7 +12,7 @@ const withNextIntl = createNextIntlPlugin({ const withMdx = mdxPlugin(); export default withMdx( - withNextIntl({, + withNextIntl({ eslint: { ignoreDuringBuilds: true }, From b48309259ababcd752daf900aadfa454765f109f Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Thu, 14 Nov 2024 11:02:20 +0100 Subject: [PATCH 23/28] remove global now --- examples/example-app-router/src/i18n/request.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/examples/example-app-router/src/i18n/request.ts b/examples/example-app-router/src/i18n/request.ts index cb01d915b..c23b287ee 100644 --- a/examples/example-app-router/src/i18n/request.ts +++ b/examples/example-app-router/src/i18n/request.ts @@ -4,11 +4,6 @@ import messages from '../../messages/en.json'; import {rootParams} from './future'; import {routing} from './routing'; -async function now() { - 'use cache'; - return new Date(); -} - export default getRequestConfig(async () => { const params = await rootParams(); const locale = hasLocale(routing.locales, params.locale) @@ -17,7 +12,6 @@ export default getRequestConfig(async () => { return { locale, - messages, - now: await now() + messages }; }); From 0172f35fd296de1675d05a4bbe819d3104359b4a Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Thu, 14 Nov 2024 15:15:10 +0100 Subject: [PATCH 24/28] revert docs changes --- docs/src/pages/docs/usage/configuration.mdx | 29 --------------------- 1 file changed, 29 deletions(-) diff --git a/docs/src/pages/docs/usage/configuration.mdx b/docs/src/pages/docs/usage/configuration.mdx index 5ef6d57a4..3e04da37b 100644 --- a/docs/src/pages/docs/usage/configuration.mdx +++ b/docs/src/pages/docs/usage/configuration.mdx @@ -461,15 +461,6 @@ When formatting [relative dates and times](/docs/usage/dates-times#relative-time ```tsx filename="i18n/request.ts" import {getRequestConfig} from 'next-intl/server'; -async function now() { - // (if you're using `dynamicIO`) - // 'use cache'; - - // Use this value consistently - // when formatting relative times - return new Date(); -} - export default getRequestConfig(async () => { return { now: new Date('2024-11-14T10:36:01.516Z') @@ -479,26 +470,6 @@ export default getRequestConfig(async () => { }); ``` -If a `now` value is provided in `i18n/request.ts`, this will automatically be inherited by Client Components if you wrap them in a `NextIntlClientProvider` that is rendered by a Server Component. - -
-How does the usage of `'use cache'` with `now` relate to cache expiration? - -If you're using [`dynamicIO`](https://nextjs.org/docs/canary/app/api-reference/config/next-config-js/dynamicIO), you can cache the value of `now` with the [`'use cache'`](https://nextjs.org/docs/canary/app/api-reference/directives/use-cache) directive: - -```tsx -async function now() { - 'use cache'; - const now = new Date(); -} -``` - -Since the request config from `i18n/request.ts` is shared among all Server Components that use features from `next-intl`, the cache expiration for `now` will apply to all of these, regardless of if they're using relative time formatting or not. - -If you want more granular cache control, you can consider passing the `now` value to [`format.relativeTime`](/docs/usage/dates-times#relative-times) explicitly as a second argument where relevant. - -
-
From 7e09d85bf44605ccf67fb6ef28a1a3a05576fbdf Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Tue, 19 Nov 2024 11:16:45 +0100 Subject: [PATCH 25/28] small improvement --- examples/example-app-router/src/app/[locale]/layout.tsx | 9 ++++++--- examples/example-app-router/src/app/layout.tsx | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/example-app-router/src/app/[locale]/layout.tsx b/examples/example-app-router/src/app/[locale]/layout.tsx index 1cde913ec..5c39147c1 100644 --- a/examples/example-app-router/src/app/[locale]/layout.tsx +++ b/examples/example-app-router/src/app/[locale]/layout.tsx @@ -1,12 +1,15 @@ import {notFound} from 'next/navigation'; import {hasLocale} from 'next-intl'; -import {getLocale, getTranslations} from 'next-intl/server'; +import {getTranslations} from 'next-intl/server'; import {ReactNode} from 'react'; import BaseLayout from '@/components/BaseLayout'; import {routing} from '@/i18n/routing'; type Props = { children: ReactNode; + params: Promise<{ + locale: string; + }>; }; export function generateStaticParams() { @@ -21,9 +24,9 @@ export async function generateMetadata() { }; } -export default async function LocaleLayout({children}: Props) { +export default async function LocaleLayout({children, params}: Props) { // This is only necessary as long as there's no `dynamicParams = false` - const locale = await getLocale(); + const {locale} = await params; if (!hasLocale(routing.locales, locale)) { notFound(); } diff --git a/examples/example-app-router/src/app/layout.tsx b/examples/example-app-router/src/app/layout.tsx index aae3d9f0c..a9dd8d0eb 100644 --- a/examples/example-app-router/src/app/layout.tsx +++ b/examples/example-app-router/src/app/layout.tsx @@ -8,5 +8,6 @@ type Props = { // Since we have a `not-found.tsx` page on the root, a layout file // is required, even if it's just passing children through. export default function RootLayout({children}: Props) { + // TODO: Can we get rid of this? return children; } From 353fa1789d6a646c20f919c5dd44a82b2958d597 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Tue, 19 Nov 2024 12:23:14 +0100 Subject: [PATCH 26/28] use case for overriding locale [skip ci] --- examples/example-app-router/src/i18n/request.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/examples/example-app-router/src/i18n/request.ts b/examples/example-app-router/src/i18n/request.ts index c23b287ee..2b9a2f525 100644 --- a/examples/example-app-router/src/i18n/request.ts +++ b/examples/example-app-router/src/i18n/request.ts @@ -15,3 +15,20 @@ export default getRequestConfig(async () => { messages }; }); + +// Use case for overriding locale: +// export default getRequestConfig(async (args) => { +// const params = await rootParams(); +// +// let locale = args.locale; +// if (!locale) { +// locale = hasLocale(routing.locales, params.locale) +// ? params.locale +// : routing.defaultLocale; +// } +// +// return { +// locale, +// messages +// }; +// }); From 5e7a79e5b0b9c1587051c0b727ebc3a991b9ddbe Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 28 Feb 2025 13:03:02 +0100 Subject: [PATCH 27/28] wip --- examples/example-app-router/src/app/manifest.ts | 5 +++-- examples/example-app-router/src/i18n/future.ts | 13 ------------- examples/example-app-router/src/i18n/request.ts | 2 +- 3 files changed, 4 insertions(+), 16 deletions(-) delete mode 100644 examples/example-app-router/src/i18n/future.ts diff --git a/examples/example-app-router/src/app/manifest.ts b/examples/example-app-router/src/app/manifest.ts index 4c740a5fe..a928c1ba5 100644 --- a/examples/example-app-router/src/app/manifest.ts +++ b/examples/example-app-router/src/app/manifest.ts @@ -1,11 +1,12 @@ import {MetadataRoute} from 'next'; +import {getTranslations} from 'next-intl/server'; export default async function manifest(): Promise { - // const t = await getTranslations('Manifest'); + const t = await getTranslations('Manifest'); return { // InvariantError: Invariant: Missing Client Reference Manifest for /manifest.webmanifest. This is a bug in Next.js. - // name: t('name'), + name: t('name'), start_url: '/', theme_color: '#101E33' }; diff --git a/examples/example-app-router/src/i18n/future.ts b/examples/example-app-router/src/i18n/future.ts deleted file mode 100644 index 7568aa23d..000000000 --- a/examples/example-app-router/src/i18n/future.ts +++ /dev/null @@ -1,13 +0,0 @@ -export async function rootParams(): Promise<{ - locale?: string; -}> { - // Outside of `[locale]` - // return Promise.resolve({ - // locale: undefined - // }); - - // In `[locale]` - return Promise.resolve({ - locale: 'en' - }); -} diff --git a/examples/example-app-router/src/i18n/request.ts b/examples/example-app-router/src/i18n/request.ts index 2b9a2f525..b01004ead 100644 --- a/examples/example-app-router/src/i18n/request.ts +++ b/examples/example-app-router/src/i18n/request.ts @@ -1,7 +1,7 @@ import {hasLocale} from 'next-intl'; import {getRequestConfig} from 'next-intl/server'; import messages from '../../messages/en.json'; -import {rootParams} from './future'; +import {unstable_rootParams as rootParams} from 'next/server'; import {routing} from './routing'; export default getRequestConfig(async () => { From 7a4da910426084a7292072cfb3549e99ba8ddac1 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 28 Feb 2025 13:09:14 +0100 Subject: [PATCH 28/28] remove some stuff so rootParams works --- examples/example-app-router/src/app/layout.tsx | 12 ------------ examples/example-app-router/src/app/manifest.ts | 1 - .../example-app-router/src/app/not-found.tsx | 17 ----------------- examples/example-app-router/src/app/page.tsx | 6 ------ 4 files changed, 36 deletions(-) delete mode 100644 examples/example-app-router/src/app/layout.tsx delete mode 100644 examples/example-app-router/src/app/not-found.tsx delete mode 100644 examples/example-app-router/src/app/page.tsx diff --git a/examples/example-app-router/src/app/layout.tsx b/examples/example-app-router/src/app/layout.tsx deleted file mode 100644 index 32ec2ce44..000000000 --- a/examples/example-app-router/src/app/layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import {ReactNode} from 'react'; - -type Props = { - children: ReactNode; -}; - -// Since we have a `not-found.tsx` page on the root, a layout file -// is required, even if it's just passing children through. -export default function RootLayout({children}: Props) { - // TODO: Can we get rid of this? - return children; -} diff --git a/examples/example-app-router/src/app/manifest.ts b/examples/example-app-router/src/app/manifest.ts index a928c1ba5..bf5f77980 100644 --- a/examples/example-app-router/src/app/manifest.ts +++ b/examples/example-app-router/src/app/manifest.ts @@ -5,7 +5,6 @@ export default async function manifest(): Promise { const t = await getTranslations('Manifest'); return { - // InvariantError: Invariant: Missing Client Reference Manifest for /manifest.webmanifest. This is a bug in Next.js. name: t('name'), start_url: '/', theme_color: '#101E33' diff --git a/examples/example-app-router/src/app/not-found.tsx b/examples/example-app-router/src/app/not-found.tsx deleted file mode 100644 index 47de61028..000000000 --- a/examples/example-app-router/src/app/not-found.tsx +++ /dev/null @@ -1,17 +0,0 @@ -'use client'; - -import Error from 'next/error'; - -// This page renders when a route like `/unknown.txt` is requested. -// In this case, the layout at `app/[locale]/layout.tsx` receives -// an invalid value as the `[locale]` param and calls `notFound()`. - -export default function GlobalNotFound() { - return ( - - - ; - - - ); -} diff --git a/examples/example-app-router/src/app/page.tsx b/examples/example-app-router/src/app/page.tsx deleted file mode 100644 index d5d37ccca..000000000 --- a/examples/example-app-router/src/app/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import {redirect} from 'next/navigation'; - -// This page only renders when the app is built statically (output: 'export') -export default function RootPage() { - redirect('/en'); -}