diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 48ee27b72..0c3398a42 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -29,9 +29,8 @@ body: label: Mandatory reproduction URL (CodeSandbox or GitHub repository) description: | **Templates:** - - [CodeSandbox (`app` directory)](https://codesandbox.io/p/sandbox/next-intl-bug-template-forked-yow8ep) - - [CodeSandbox (`app` directory, RSC beta)](https://codesandbox.io/p/sandbox/next-intl-bug-template-app-forked-zcymvq) - - [CodeSandbox (`pages` directory)](https://codesandbox.io/p/sandbox/next-intl-bug-template-pages-krm37f) + - [CodeSandbox (App Router)](https://codesandbox.io/p/sandbox/next-intl-bug-template-app-forked-zcymvq) + - [CodeSandbox (Pages Router)](https://codesandbox.io/p/sandbox/next-intl-bug-template-pages-krm37f) validations: required: true - type: textarea diff --git a/docs/components/CodeSnippets.tsx b/docs/components/CodeSnippets.tsx index afbc76ec2..246546186 100644 --- a/docs/components/CodeSnippets.tsx +++ b/docs/components/CodeSnippets.tsx @@ -348,7 +348,7 @@ function buildOutput() { kB {' '} - 104 kB + 87.6 kB @@ -358,7 +358,7 @@ function buildOutput() { B {' '} - 97.5 kB + 80.2 kB @@ -368,7 +368,7 @@ function buildOutput() { kB {' '} - 106 kB + 89.3 kB diff --git a/docs/components/VersionTabs.tsx b/docs/components/VersionTabs.tsx deleted file mode 100644 index 8f1f4377b..000000000 --- a/docs/components/VersionTabs.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import {Tabs} from 'nextra-theme-docs'; -import {ReactNode} from 'react'; -import Chip from './Chip'; - -type Props = { - children: ReactNode; - defaultLabel?: ReactNode; - rscLabel?: ReactNode; -}; - -export default function VersionTabs({ - children, - defaultLabel = 'Default', - rscLabel = 'Server Components' -}: Props) { - return ( - - {rscLabel} - - Beta - - - ]} - > - {children} - - ); -} diff --git a/docs/next.config.js b/docs/next.config.js index e48e5a88f..48d6f8710 100644 --- a/docs/next.config.js +++ b/docs/next.config.js @@ -30,12 +30,17 @@ module.exports = withNextra({ }, { source: '/docs/getting-started/production-checklist', - destination: '/docs/production-checklist', + destination: '/docs/environments/runtime-requirements', permanent: true }, { source: '/docs/usage/production-checklist', - destination: '/docs/production-checklist', + destination: '/docs/environments/runtime-requirements', + permanent: true + }, + { + source: '/docs/production-checklist', + destination: '/docs/environments/runtime-requirements', permanent: true }, { @@ -45,12 +50,22 @@ module.exports = withNextra({ }, { source: '/docs/next-13/client-components', - destination: '/docs/getting-started/app-router-client-components', + destination: '/docs/getting-started/app-router', permanent: true }, { source: '/docs/next-13/server-components', - destination: '/docs/getting-started/app-router-server-components', + destination: '/docs/getting-started/app-router', + permanent: true + }, + { + source: '/docs/getting-started/app-router-server-components', + destination: '/docs/getting-started/app-router', + permanent: true + }, + { + source: '/docs/getting-started/app-router-client-components', + destination: '/docs/getting-started/app-router', permanent: true }, { @@ -75,22 +90,17 @@ module.exports = withNextra({ }, { source: '/docs/usage/production-checklist', - destination: '/docs/production-checklist', + destination: '/docs/environments/runtime-requirements', permanent: true }, { source: '/docs/usage/runtime-requirements-polyfills', - destination: '/docs/production-checklist#runtime-requirements', - permanent: true - }, - { - source: '/docs/usage/configuration', - destination: '/docs/configuration', + destination: '/docs/environments/runtime-requirements', permanent: true }, { source: '/docs/usage/error-handling', - destination: '/docs/configuration#error-handling', + destination: '/docs/usage/configuration#error-handling', permanent: true }, { diff --git a/docs/pages/blog/index.mdx b/docs/pages/blog/index.mdx index 087f72ced..80eb6abf9 100644 --- a/docs/pages/blog/index.mdx +++ b/docs/pages/blog/index.mdx @@ -2,8 +2,6 @@ import CommunityLink from 'components/CommunityLink'; # next-intl blog -Failed attempts at writing docs that somehow turned into blog posts. -
@@ -125,11 +120,7 @@ When an `error` file is defined, Next.js creates [an error boundary within your -Since the `error` file must be defined as a Client Component, you have to use [`NextIntlClientProvider`](/docs/configuration#client-server-components) to provide messages in case the `error` file renders. - -If you've [set up `next-intl` to be used in Client Components](/docs/getting-started/app-router-client-components), this is already the case and there's no additional setup needed. If you're using [the Server Components beta](/docs/getting-started/app-router-server-components) though, you have to provide the relevant messages in the wrapping layout. - -
+Since the `error` file must be defined as a Client Component, you have to use [`NextIntlClientProvider`](/docs/usage/configuration#nextintlclientprovider) to provide messages in case the `error` file renders. ```tsx filename="app/[locale]/layout.tsx" import pick from 'lodash/pick'; @@ -153,13 +144,6 @@ export default async function LocaleLayout({children}) { } ``` -
- Providing messages for the `error` file is only necessary when using [the - Server Components beta](/docs/getting-started/app-router-server-components). -
- -
- Once `NextIntlClientProvider` is in place, you can use functionality from `next-intl` in the `error` file: ```tsx filename="app/[locale]/error.tsx" @@ -178,3 +162,5 @@ export default function Error({error, reset}) { ); } ``` + +Note that `NextIntlClientProvider` only provides the messages to your error page, but doesn't load any runtime code for processing translations on the client side. Only the error page will include this code, so the performance of other pages isn't affected. diff --git a/docs/pages/docs/environments/index.mdx b/docs/pages/docs/environments/index.mdx index 4ca310a98..ee525c26e 100644 --- a/docs/pages/docs/environments/index.mdx +++ b/docs/pages/docs/environments/index.mdx @@ -36,3 +36,5 @@ The `next-intl` APIs are available in the following environments: href="/docs/environments/core-library" />
+ +While modern browsers and server runtimes typically support all necessary JavaScript APIs that are required for `next-intl`, you can double check [the runtime requirements](/docs/environments/runtime-requirements). diff --git a/docs/pages/docs/environments/metadata-route-handlers.mdx b/docs/pages/docs/environments/metadata-route-handlers.mdx index 0e1a17591..f92c46a42 100644 --- a/docs/pages/docs/environments/metadata-route-handlers.mdx +++ b/docs/pages/docs/environments/metadata-route-handlers.mdx @@ -1,5 +1,4 @@ import Callout from 'components/Callout'; -import VersionTabs from 'components/VersionTabs'; import {Tab} from 'nextra-theme-docs'; # Internationalization of Metadata & Route Handlers in Next.js 13 @@ -10,29 +9,7 @@ There are a few places in Next.js apps where you might need to apply internation 2. [Metadata files](https://nextjs.org/docs/app/api-reference/file-conventions/metadata) 3. [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) -For these cases, you can either use: - -1. The [core library](/docs/environments/core-library) from `next-intl`, if you're using [the latest stable version](/docs/getting-started/app-router-client-components) -2. A set of new APIs that automatically pick up your request configuration, if you're using [the Server Components beta version](/docs/getting-started/app-router-server-components) - - - - -```tsx -import {createTranslator, createFormatter} from 'next-intl'; - -// The `locale` is received from Next.js via `params` -const locale = params.locale; - -// You can use the core (non-React) APIs when you -// have to use next-intl outside of components. -const t = createTranslator({locale, messages}); -const format = createFormatter({locale: 'en'}); -``` - - - -`next-intl/server` provides a set of awaitable versions of the functions that you usually call as hooks from within components. Unlike the hooks, these functions require a `locale` that you receive from Next.js. +`next-intl/server` provides a set of awaitable versions of the functions that you usually call as hooks from within components. Unlike the hooks, these functions require a `locale` that you [receive from Next.js](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes). ```tsx import { @@ -54,38 +31,15 @@ const messages = await getMessages(locale); ``` - The [global request configuration]((#global-request-configuration)) that - you've set up in `i18n.ts` is automatically inherited by these functions. The - `locale` is the only exception that needs to be provided in comparison to the - hooks. + The request configuration that you've set up in `i18n.ts` is automatically + inherited by these functions. The `locale` is the only exception that needs to + be provided in comparison to the hooks. - - - ### Metadata API To internationalize metadata like the page title, you can use functionality from `next-intl` in the [`generateMetadata`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#generatemetadata-function) function that can be exported from pages and layouts. - - - -```tsx filename="app/[locale]/layout.tsx" -import {createTranslator} from 'next-intl'; - -export async function generateMetadata({params: {locale}}) { - const messages = (await import(`../../../messages/${locale}.json`)).default; - const t = createTranslator({locale, messages}); - - return { - title: t('LocaleLayout.title') - }; -} -``` - - - - ```tsx filename="app/[locale]/layout.tsx" import {getTranslator} from 'next-intl/server'; @@ -98,31 +52,10 @@ export async function generateMetadata({params: {locale}}) { } ``` - - - ### Metadata files If you need to internationalize content within [metadata files](https://nextjs.org/docs/app/api-reference/file-conventions/metadata), such as an Open Graph image, you can call APIs from `next-intl` in the exported function. - - - -```tsx filename="app/[locale]/opengraph-image.tsx" -import {ImageResponse} from 'next/server'; -import {createTranslator} from 'next-intl'; - -export default async function Image({params: {locale}}) { - const messages = (await import(`../../../messages/${locale}.json`)).default; - const t = createTranslator({locale, messages, namespace: 'OpenGraph'}); - - return new ImageResponse(
{t('title')}
); -} -``` - -
- - ```tsx filename="app/[locale]/opengraph-image.tsx" import {ImageResponse} from 'next/server'; import {getTranslator} from 'next-intl/server'; @@ -133,39 +66,19 @@ export default async function Image({params: {locale}}) { } ``` - -
- ### Route Handlers -If you put [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) in your `[locale]` folder, you can use functionality from `next-intl` to localize the response based on the `locale`. Note however that you should make sure that the route is matched by your [middleware `config`](/docs/routing/middleware). - - - +You can use `next-intl` in [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) too. The required `locale` can either be received from a search param, a layout segment or by parsing the `accept-language` header of the request. -```tsx filename="app/[locale]/hello/route.tsx" -import {NextResponse} from 'next/server'; -import {createTranslator} from 'next-intl'; - -export async function GET(request, {params: {locale}}) { - const messages = (await import(`../../../messages/${locale}.json`)).default; - const t = createTranslator({locale, messages, namespace: 'Hello'}); - return NextResponse.json({title: t('title')}); -} -``` - - - - -```tsx filename="app/[locale]/hello/route.tsx" +```tsx filename="app/api/hello/route.tsx" import {NextResponse} from 'next/server'; import {getTranslator} from 'next-intl/server'; -export async function GET(request, {params: {locale}}) { +export async function GET(request) { + const {searchParams} = new URL(request.url); + const locale = searchParams.get('locale'); + const t = await getTranslator(locale, 'Hello'); return NextResponse.json({title: t('title')}); } ``` - - - diff --git a/docs/pages/docs/production-checklist.mdx b/docs/pages/docs/environments/runtime-requirements.mdx similarity index 66% rename from docs/pages/docs/production-checklist.mdx rename to docs/pages/docs/environments/runtime-requirements.mdx index 06f5a8f9d..b1b01d6cb 100644 --- a/docs/pages/docs/production-checklist.mdx +++ b/docs/pages/docs/environments/runtime-requirements.mdx @@ -1,18 +1,8 @@ import Callout from 'components/Callout'; -# Production checklist +# Runtime requirements -While the [installation instructions](/docs/getting-started) are sufficient to use `next-intl` in your Next.js app, this checklist helps you ensure you're all set for production: - -1. If you're using TypeScript, you can take advantage of autocompletion and type safety for message keys by [setting up a type for your messages](/docs/workflows/typescript). -2. If you're formatting dates and times, a [time zone should be configured](/docs/configuration#time-zone). By default, dates are formatted according to the time zone of the environment, which can lead to markup mismatches if the server and the user are located in different time zones. By supplying the `timeZone` explicitly, you can ensure that dates and times are rendered the same way on the server as well as the client. -3. If you're formatting relative dates and times, a [global value for `now`](/docs/configuration#global-now-value) can be useful. This ensures that the server and client will render the same markup. Especially if you use caching for the responses of the server, the likelyhood of mismatches increases. -4. To achieve consistent date, time and number formatting, it might be useful to set up [global formats](/docs/configuration#global-formats) which ensure consistent formatting across the app. -5. Please check the [runtime requirements](#runtime-requirements) and optionally provide polyfills. - -## Runtime requirements - -### Browser +## Browser Based on the features you're using, you have to make sure your browser supports the following APIs: @@ -68,6 +58,6 @@ function IntlPolyfills() { (e.g. search for `Intl.DateTimeFormat.~locale.de-AT`). -### Node +## Node The minimum required version is **Node.js 13**. Starting from this version, all required APIs are available. diff --git a/docs/pages/docs/environments/server-client-components.mdx b/docs/pages/docs/environments/server-client-components.mdx index 8c2f30c35..e58ff014a 100644 --- a/docs/pages/docs/environments/server-client-components.mdx +++ b/docs/pages/docs/environments/server-client-components.mdx @@ -2,12 +2,6 @@ import Callout from 'components/Callout'; # Internationalization of Server & Client Components in Next.js 13 - - -This page contains background information about the advantages of moving internationalization to Server Components. Note that this is currently only available in [the Server Components beta version](/docs/getting-started/app-router-server-components). - - - With the introduction of the App Router in Next.js 13, [React Server Components](https://nextjs.org/docs/getting-started/react-essentials) became publicly available. This new paradigm allows components that don’t require React’s interactive features, such as `useState` and `useEffect`, to remain server-side only. This applies to handling internationalization too. @@ -92,7 +86,7 @@ function Expandable({title, children}) { As you see, we can use interactive features from React like `useState` on translated content, even though the translation only runs on the server side. -Learn more in the Next.js docs: [Nesting Server Components inside Client Components](https://nextjs.org/docs/getting-started/react-essentials#nesting-server-components-inside-client-components) +Learn more in the Next.js docs: [Passing Server Components to Client Components as Props](https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#supported-pattern-passing-server-components-to-client-components-as-props) ### Option 2: Moving state to the server side @@ -164,6 +158,8 @@ import {NextIntlClientProvider} from 'next-intl'; import {notFound} from 'next/navigation'; export default async function LocaleLayout({children, params: {locale}}) { + // ... + // Receive messages provided in `i18n.ts` const messages = useMessages(); @@ -201,9 +197,9 @@ You might encounter this error when you try to pass a non-serializable prop to ` The component accepts the following props that are not serializable: -1. [`onError`](/docs/configuration#error-handling) -2. [`getMessageFallback`](/docs/configuration#error-handling) -3. Rich text elements for [`defaultTranslationValues`](/docs/configuration#default-translation-values) +1. [`onError`](/docs/usage/configuration#error-handling) +2. [`getMessageFallback`](/docs/usage/configuration#error-handling) +3. Rich text elements for [`defaultTranslationValues`](/docs/usage/configuration#default-translation-values) To configure these, you can wrap `NextIntlClientProvider` with another component that is marked with `'use client'` and defines the relevant props: diff --git a/docs/pages/docs/faq.mdx b/docs/pages/docs/faq.mdx index f5b2e51c6..7017b6e4e 100644 --- a/docs/pages/docs/faq.mdx +++ b/docs/pages/docs/faq.mdx @@ -2,7 +2,7 @@ ## What trade-offs does this library make? -If you bundle `next-intl` for the client side ([which you don't have to](/docs/environments/server-client-components)), the bundle size comes in at [~14kB gzipped](https://bundlephobia.com/result?p=next-intl) which is the tradeoff that's necessary for supporting all the mentioned internationalization features. There are smaller libraries for internationalization, but they typically cover less features. +If you bundle `next-intl` for the client side ([which you don't have to](/docs/environments/server-client-components)), the bundle size comes in at [~14kB gzipped](https://bundlephobia.com/result?p=next-intl) which is the tradeoff that's necessary for supporting all the included i18n features. There are smaller libraries for internationalization, but they typically cover less features. If you don't need all features of `next-intl`, you can reduce the bundle size with tree shaking. Furthermore if [native `Intl.MessageFormat`](https://github.com/tc39/proposal-intl-messageformat) lands in JavaScript in the future, we might be able to reduce the bundle size significantly. @@ -10,7 +10,7 @@ Generally, it's recommended to [handle internationalization in Server Components ## How is this library different from using react-intl? -1. This library offers tight integration with Next.js, e.g. through [the routing integration](/docs/routing). +1. This library offers tight integration with Next.js, e.g. through [the routing integration](/docs/routing) and Server Components support. 2. This library offers a hooks-only API for message consumption. The reason for this is that the same API can be used for attributes as well as `children`. 3. This library is built around the concept of namespaces and that components consume a single namespace—however this is not required. 4. This library currently doesn't support AST-based extraction like `react-intl`. Note that building ASTs for messages upfront can help with runtime performance, but in exchange your bundle size might grow. @@ -33,4 +33,4 @@ Yes, see [the Remix example](https://github.com/amannn/next-intl/tree/main/examp ## Can `next-intl` be used with [React Native](https://reactnative.dev/)? -Yes, see [the React Native example](https://github.com/amannn/next-intl/tree/main/examples/example-react-native). +Yes, see [the React Native example](https://github.com/amannn/next-intl/tree/main/examples/example-react-native). Please double check that [the runtime requirements](/docs/environments/runtime-requirements) are fulfilled. diff --git a/docs/pages/docs/getting-started/_meta.json b/docs/pages/docs/getting-started/_meta.json index fe6fbc321..dda40d626 100644 --- a/docs/pages/docs/getting-started/_meta.json +++ b/docs/pages/docs/getting-started/_meta.json @@ -1,6 +1,5 @@ { "index": "Welcome!", - "app-router-client-components": "App Router (Client Components)", - "app-router-server-components": "App Router (Server Components)", + "app-router": "App Router", "pages-router": "Pages Router" } diff --git a/docs/pages/docs/getting-started/app-router-client-components.mdx b/docs/pages/docs/getting-started/app-router-client-components.mdx deleted file mode 100644 index db3f44866..000000000 --- a/docs/pages/docs/getting-started/app-router-client-components.mdx +++ /dev/null @@ -1,137 +0,0 @@ -import Callout from 'components/Callout'; -import Steps from 'components/Steps'; - -# Next.js 13: Internationalization (i18n) in Client Components - -Next.js 13 introduces support for [React Server Components](https://nextjs.org/docs/getting-started/react-essentials) with the App Router. While [support for Server Components in `next-intl`](/docs/getting-started/app-router-server-components) is on the horizon, you can use `next-intl` in the `app` directory by deferring the usage of internationalization to Client Components. - -## Getting started - -If you haven't done so already, [create a Next.js 13 app that uses the `app` directory](https://nextjs.org/docs/getting-started/installation). The goal is to prefix all routes with the `locale`, so that we can retrieve it as a [dynamic segment](https://nextjs.org/docs/app/building-your-application/routing/defining-routes#creating-routes) and use it to configure `next-intl`. - -**Start by running `npm install next-intl` and create the following file structure:** - -``` -├── messages (1) -│ ├── en.json -│ └── ... -├── middleware.ts (2) -└── app - └── [locale] - ├── layout.tsx (3) - └── page.tsx (4) -``` - -**Now, set up these files as follows:** - - - -### `messages/en.json` - -Messages can be provided locally or loaded from a remote data source (e.g. a translation management system). Use whatever suits your workflow best. - -The simplest option is to create JSON files locally based on locales, e.g. `en.json`. - -```json filename="messages/en.json" -{ - "Index": { - "title": "Hello world!" - } -} -``` - -### `middleware.ts` - -[The middleware](/docs/routing/middleware) matches a locale for the request and handles redirects and rewrites accordingly. - -```tsx filename="middleware.ts" -import createMiddleware from 'next-intl/middleware'; - -export default createMiddleware({ - // A list of all locales that are supported - locales: ['en', 'de'], - - // If this locale is matched, pathnames work without a prefix (e.g. `/about`) - defaultLocale: 'en' -}); - -export const config = { - // Skip all paths that should not be internationalized. This example skips - // certain folders and all pathnames with a dot (e.g. favicon.ico) - matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'] -}; -``` - -**Note:** If you have pages that contain the character `.` in the pathname (e.g. `/users/jane.doe`), you might want to consider them in your [matcher config](/docs/routing/middleware#matcher-config). - -### `app/[locale]/layout.tsx` [#next-intl-client-provider] - -Provide the document layout and set up `NextIntlClientProvider`. - -```tsx filename="app/[locale]/layout.tsx" /NextIntlClientProvider/ -import {NextIntlClientProvider} from 'next-intl'; -import {notFound} from 'next/navigation'; - -export function generateStaticParams() { - return [{locale: 'en'}, {locale: 'de'}]; -} - -export default async function LocaleLayout({children, params: {locale}}) { - let messages; - try { - messages = (await import(`../../messages/${locale}.json`)).default; - } catch (error) { - notFound(); - } - - return ( - - - - {children} - - - - ); -} -``` - -### `app/[locale]/page.tsx` [#usage] - -Turn your page component into a Client Component to be able to use translations. - -```tsx filename="app/[locale]/page.tsx" -'use client'; - -import {useTranslations} from 'next-intl'; - -export default function Index() { - const t = useTranslations('Index'); - return

{t('title')}

; -} -``` - -
- -That's all you need to do to start using translations in the `app` directory! - -Note that you have to mark all components that use features from `next-intl` as Client Components if you use this approach. Support for `next-intl` APIs in Server Components is [available in a beta version](/docs/getting-started/app-router-server-components). - - - -**Next steps:** - - - - diff --git a/docs/pages/docs/getting-started/app-router-server-components.mdx b/docs/pages/docs/getting-started/app-router.mdx similarity index 51% rename from docs/pages/docs/getting-started/app-router-server-components.mdx rename to docs/pages/docs/getting-started/app-router.mdx index 91a741246..b83e5a8fd 100644 --- a/docs/pages/docs/getting-started/app-router-server-components.mdx +++ b/docs/pages/docs/getting-started/app-router.mdx @@ -1,35 +1,9 @@ import Callout from 'components/Callout'; import Steps from 'components/Steps'; -# Next.js 13: Internationalization (i18n) in Server Components +# Next.js 13: Internationalization (i18n) -Next.js 13 introduces support for [React Server Components](https://nextjs.org/docs/getting-started/react-essentials) with the App Router and unlocks [many benefits](/docs/environments/server-client-components) when handling internationalization entirely on the server side. `next-intl` is adopting the new capabilities and is currently offering a beta version to early adopters, who are already building apps with Server Components. - - - Support for React Server Components is currently in beta. Please use it at - your own risk, knowing that you may have to migrate upon a stable release. - - -## Current beta version - -``` -npm install next-intl@3.0.0-beta.19 -``` - -This beta version was tested with `next@13.5.1`. - -## Roadmap - -| Feature | Status | -| :------------------------------------------------- | :----: | -| Usage of all `next-intl` APIs in Server Components | ✅ | -| Dynamic rendering | ✅ | -| Static rendering (i.e. `generateStaticParams`) | 🏗️ | - - - Support for static rendering is currently available via a stopgap solution - (see [static rendering](#static-rendering). - +Next.js 13 introduces support for [React Server Components](https://nextjs.org/docs/getting-started/react-essentials) with the App Router and unlocks [many benefits](/docs/environments/server-client-components) when handling internationalization on the server side. ## Getting started @@ -41,13 +15,14 @@ If you haven't done so already, [create a Next.js 13 app that uses the App Route ├── messages (1) │ ├── en.json │ └── ... -├── i18n.ts (2) -├── next.config.js (3) -├── middleware.ts (4) -└── app - └── [locale] - ├── layout.tsx (5) - └── page.tsx (6) +├── next.config.js (2) +└── src + ├── i18n.ts (3) + ├── middleware.ts (4) + └── app + └── [locale] + ├── layout.tsx (5) + └── page.tsx (6) ``` **Now, set up the files as follows:** @@ -58,7 +33,7 @@ If you haven't done so already, [create a Next.js 13 app that uses the App Route Messages can be provided locally or loaded from a remote data source (e.g. a translation management system). Use whatever suits your workflow best. -The simplest option is to create JSON files locally based on locales, e.g. `en.json`. +The simplest option is to add JSON files in your project based on locales, e.g. `en.json`. ```json filename="messages/en.json" { @@ -68,26 +43,41 @@ The simplest option is to create JSON files locally based on locales, e.g. `en.j } ``` -### `i18n.ts` +### `next.config.js` + +Now, set up the plugin which provides i18n configuration for Server Components. + +```js filename="next.config.js" +const withNextIntl = require('next-intl/plugin')(); + +module.exports = withNextIntl({ + // Other Next.js configuration ... +}); +``` + +### `src/i18n.ts` [#i18nts] -`next-intl` creates a configuration once per request and makes it available to all Server Components. Here you can provide messages and other options depending the locale of the user. +`next-intl` creates a configuration once per request. Here you can provide messages and other options depending on the locale of the user. -```tsx filename="i18n.ts" +```tsx filename="src/i18n.ts" import {getRequestConfig} from 'next-intl/server'; export default getRequestConfig(async ({locale}) => ({ - messages: (await import(`./messages/${locale}.json`)).default + messages: (await import(`../messages/${locale}.json`)).default })); ``` -### `next.config.js` +
+ Can I move this file somewhere else? -Now, set up the plugin and provide the path to your `i18n.ts` file. +This file is supported out-of-the-box both in the `src` folder as well as in the project root with the extensions `.ts`, `.tsx`, `.js` and `.jsx`. + +If you prefer to move this file somewhere else, you can provide an optional path to the plugin: ```js filename="next.config.js" const withNextIntl = require('next-intl/plugin')( - // This is the default (also the `src` folder is supported out of the box) - './i18n.ts' + // Specify a custom path here + './somewhere/else/i18n.ts' ); module.exports = withNextIntl({ @@ -95,6 +85,8 @@ module.exports = withNextIntl({ }); ``` +
+ ### `middleware.ts` [The middleware](/docs/routing/middleware) matches a locale for the request and handles redirects and rewrites accordingly. @@ -106,19 +98,16 @@ export default createMiddleware({ // A list of all locales that are supported locales: ['en', 'de'], - // If this locale is matched, pathnames work without a prefix (e.g. `/about`) + // Used when no locale matches defaultLocale: 'en' }); export const config = { - // Skip all paths that should not be internationalized. This example skips - // certain folders and all pathnames with a dot (e.g. favicon.ico) - matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'] + // Match only internationalized pathnames + matcher: ['/', '/(de|en)/:path*'] }; ``` -**Note:** If you have pages that contain the character `.` in the pathname (e.g. `/users/jane.doe`), you might want to consider them in your [matcher config](/docs/routing/middleware#matcher-config). - ### `app/[locale]/layout.tsx` The `locale` that was matched by the middleware is available via the `locale` param and can be used to configure the document language. @@ -131,8 +120,7 @@ const locales = ['en', 'de']; export default function LocaleLayout({children, params: {locale}}) { // Validate that the incoming `locale` parameter is valid - const isValidLocale = locales.some((cur) => cur === locale); - if (!isValidLocale) notFound(); + if (!locales.includes(locale as any)) notFound(); return ( @@ -157,42 +145,30 @@ export default function Index() { -That's all it takes! Now you can internationalize your apps on the server side. +That's all it takes! **Next steps:** ## Static rendering -The support for using `next-intl` in React Server Components currently opts your pages into dynamic rendering. This is a limitation that will eventually be lifted once [`createServerContext`](https://react.dev/blog/2023/05/03/react-canaries#announcing-breaking-changes-and-new-features-early) is available and integrated in Next.js. +By using APIs like `useTranslations` from `next-intl` in Server Components, your pages will currently opt into dynamic rendering. This is a limitation that will eventually be lifted once [`createServerContext`](https://react.dev/blog/2023/05/03/react-canaries#announcing-breaking-changes-and-new-features-early) is available and integrated in Next.js. -If you have a strong need for static rendering, you can enable static rendering as follows: +As a stopgap solution, `next-intl` provides a temporary API that can be used to enable static rendering: @@ -210,7 +186,7 @@ export function generateStaticParams() { ### Add `unstable_setRequestLocale` to all layouts and pages -As a stopgap solution, `next-intl` provides a temporary API that can be used to distribute the locale that is received via `params` in a layout or page for usage in all Server Components that are rendered as part of the request. +`next-intl` provides a temporary API that can be used to distribute the locale that is received via `params` in a layout or page for usage in all Server Components that are rendered as part of the request. ```tsx filename="app/[locale]/layout.tsx" import {unstable_setRequestLocale} from 'next-intl/server'; @@ -222,8 +198,7 @@ export default async function LocaleLayout({ params: {locale} }) { // Validate that the incoming `locale` parameter is valid - const isValidLocale = locales.some((cur) => cur === locale); - if (!isValidLocale) notFound(); + if (!locales.includes(locale as any)) notFound(); unstable_setRequestLocale(locale); @@ -248,18 +223,12 @@ export default async function IndexPage({ } ``` + + **What does "unstable" mean?** -`unstable_setRequestLocale` is meant to be used as a stopgap solution and will eventually be replaced by an API that's based on `createServerContext`. When that time comes, you'll get a deprecation notice in a minor version and the API will be removed as part of a major version. +`unstable_setRequestLocale` is meant to be used as a stopgap solution and will eventually be replaced by an API that's based on [`createServerContext`](https://react.dev/blog/2023/05/03/react-canaries#announcing-breaking-changes-and-new-features-early). When that time comes, you'll get a deprecation notice in a minor version and the API will be removed as part of a major version. Note that Next.js can render layouts and pages indepently. This means that e.g. when you navigate from `/settings/profile` to `/settings/privacy`, the `/settings` segment might not re-render as part of the request. Due to this, it's important that `unstable_setRequestLocale` is called not only in the parent `settings/layout.tsx`, but also in the individual pages `profile/page.tsx` and `settings/page.tsx`. -Apart from that, the API can only be used for pages that receive `params` (i.e. not `not-found.tsx`). - That being said, the API is expected to work reliably if you're cautious to apply it in all relevant places. - - - -## Providing feedback - -If you have feedback about using `next-intl` in the `app` directory, feel free to leave feedback in [the PR that implements the React Server Components support](https://github.com/amannn/next-intl/pull/149). diff --git a/docs/pages/docs/getting-started/index.mdx b/docs/pages/docs/getting-started/index.mdx index 261a3a63e..4d593c55e 100644 --- a/docs/pages/docs/getting-started/index.mdx +++ b/docs/pages/docs/getting-started/index.mdx @@ -13,35 +13,13 @@ Welcome to the `next-intl` docs! In this guide you will learn how to set up internationalization (i18n) in your Next.js app. -With **Next.js 13**, the [App Router](https://nextjs.org/docs/app) was introduced and announced as stable with version 13.4. Following the lead of Next.js, `next-intl` now also recommends this paradigm going forward. - - - Support for using all `next-intl` APIs in Server Components is currently in - beta. If you're fine with using internationalization in Client Components, you - can use the latest stable release of `next-intl`, but if you're an early - adopter, you may be interested in the Server Components beta. - +With **Next.js 13**, the [App Router](https://nextjs.org/docs/app) along with support for React Server Components was introduced and announced as stable with version 13.4. Following the lead of Next.js, `next-intl` also recommends this paradigm since it increases both simplicity as well as flexibility when it comes to i18n.
} - title={ - - Usage with the App Router and Client Components - Stable - - } - href="/docs/getting-started/app-router-client-components" - /> - } - title={ - - Usage with the App Router and Server Components - Beta - - } - href="/docs/getting-started/app-router-server-components" + title="Usage with the App Router" + href="/docs/getting-started/app-router" /> } diff --git a/docs/pages/docs/getting-started/pages-router.mdx b/docs/pages/docs/getting-started/pages-router.mdx index d7d91303b..0287542d3 100644 --- a/docs/pages/docs/getting-started/pages-router.mdx +++ b/docs/pages/docs/getting-started/pages-router.mdx @@ -3,7 +3,7 @@ import Callout from 'components/Callout'; # Next.js 13 internationalization (i18n) with the Pages Router -While it's recommended to [use `next-intl` with the App Router](/docs/getting-started), the Pages Router is still fully supported. +While it's recommended to [use `next-intl` with the App Router](/docs/getting-started/app-router), the Pages Router is still fully supported. @@ -19,7 +19,10 @@ export default function App({Component, pageProps}) { const router = useRouter(); return ( - + ); @@ -90,14 +93,15 @@ export async function getStaticProps() {
  • Exploring `next-intl`? Check out the [usage guide](/docs/usage).
  • -
  • - Decided you're sticking with `next-intl`? Consider the steps of the - [checklist for production](/docs/production-checklist). -
  • Ran into an issue? Have a look at [the Pages Router example](/examples/pages-router) to explore a working app.
  • +
  • + Are you migrating to the App Router? See [the migration + example](https://github.com/amannn/next-intl/tree/main/examples/example-next-13-with-pages) + that uses both the App Router as well as the Pages router. +
diff --git a/docs/pages/docs/routing/index.mdx b/docs/pages/docs/routing/index.mdx index 042d8ee18..e14a39abf 100644 --- a/docs/pages/docs/routing/index.mdx +++ b/docs/pages/docs/routing/index.mdx @@ -3,7 +3,7 @@ import {Card} from 'nextra-theme-docs'; # Internationalized routing -When you provide content in multiple languages, you want to make the content available under distinct URLs (e.g. `/en/about`). `next-intl` provides the building blocks to set up internationalized routing as well as the navigation APIs to enable you to link between pages. +When you provide content in multiple languages, you want to make your pages available under distinct pathnames (e.g. `/en/about`). `next-intl` provides the building blocks to set up internationalized routing as well as the navigation APIs to enable you to link between pages.
Switch to English` to allow the user to change the locale to `en`. -4. When the user clicks on the link, a request to `/en` is initiated. -5. The middleware will update the cookie value to `en` and subsequently redirects the user to `/`. +1. A user requests `/` and based on the `accept-language` header, the `en` locale is matched. +2. The `en` locale is saved in a cookie and the user is redirected to `/en`. +3. The app renders `Switch to German` to allow the user to change the locale to `de`. +4. When the user clicks on the link, a request to `/de` is initiated. +5. The middleware will update the cookie value to `de`. + + + You can optionally remove the locale prefix in pathnames by changing the + [`localePrefix`](#locale-prefix) setting. + ### Strategy 2: Domain-based routing [#domain-based-routing] If you want to serve your localized content based on different domains, you can provide a list of mappings between domains and locales to the middleware. -**Example:** +**Examples:** -- `us.example.com` (default: `en`) -- `ca.example.com` (default: `en`) -- `ca.example.com/fr` (`fr`) +- `us.example.com/en` +- `ca.example.com/en` +- `ca.example.com/fr` ```tsx filename="middleware.ts" import createMiddleware from 'next-intl/middleware'; @@ -101,7 +99,10 @@ export default createMiddleware({ }); ``` -The middleware rewrites the requests internally, so that requests for the `defaultLocale` on a given domain work without a locale prefix (e.g. `us.example.com/about` → `/en/about`). If you want to include a prefix for the default locale as well, you can add [`localePrefix: 'always'`](#always-use-a-locale-prefix). + + You can optionally remove the locale prefix in pathnames by changing the + [`localePrefix`](#locale-prefix) setting. + #### Locale detection @@ -113,12 +114,6 @@ The locale is detected based on these priorities: 2. If the host of the request is configured in `domains`, the `defaultLocale` of the domain is used 3. As a fallback, the [locale detection of prefix-based routing](#locale-detection) applies - - -Since unknown domains will be handled with [prefix-based routing](#prefix-based-routing), this strategy can be used for local development where the host is `localhost`. - - - Since the middleware is aware of all your domains, the domain will automatically be switched when the user requests to change the locale. **Example workflow:** @@ -128,11 +123,40 @@ Since the middleware is aware of all your domains, the domain will automatically 3. When the link is clicked, a request to `us.example.com/fr` is initiated. 4. The middleware recognizes that the user wants to switch to another domain and responds with a redirect to `ca.example.com/fr`. +
+How is the best matching domain for a given locale detected? + +The bestmatching domain is detected based on these priorities: + +1. Stay on the current domain if the locale is supported here +2. Use an alternative domain where the locale is configured as the `defaultLocale` +3. Use an alternative domain where the available `locales` are restricted and the locale is supported +4. Stay on the current domain if it supports all locales +5. Use an alternative domain that supports all locales + +
+ ## Further configuration -### Always use a locale prefix +### Locale prefix + +#### Always use a locale prefix [#locale-prefix-always] + +By default, pathnames always start with the locale (e.g. `/en/about`). + +```tsx filename="middleware.ts" {6} +import createMiddleware from 'next-intl/middleware'; + +export default createMiddleware({ + // ... other config + + localePrefix: 'always' // This is the default +}); +``` + +#### Don't use a locale prefix for the default locale [#locale-prefix-as-necessary] -If you want to include a prefix for the default locale as well, you can configure the middleware accordingly. +If you don't want to include a locale prefix for the default locale, but only for non-default locales, you can configure the middleware accordingly. ```tsx filename="middleware.ts" {6} import createMiddleware from 'next-intl/middleware'; @@ -140,15 +164,23 @@ import createMiddleware from 'next-intl/middleware'; export default createMiddleware({ // ... other config - localePrefix: 'always' + localePrefix: 'as-necessary' }); ``` -In this case, requests without a prefix will be redirected accordingly (e.g. `/about` to `/en/about`). +In this case, requests where the locale prefix matches the default locale will be redirected (e.g. `/en/about` to `/about`). This will affect both prefix-based as well as domain-based routing. -Note that this will affect both prefix-based as well as domain-based routing. +**Important:** If you use this strategy, you should make sure that [your matcher detects unprefixed pathnames](#matcher-no-prefix). -### Never use a locale prefix + + If you use [the `Link` component](/docs/routing/navigation#link), the initial + render will point to the prefixed version but will be patched immediately on + the client once the component detects that the default locale has rendered. + The prefixed version is still valid, but SEO tools might report a hint that + the link points to a redirect. + + +#### Never use a locale prefix [#locale-prefix-never] For applications behind an authentication layer, where there is no need for SEO, it is possible to have the locale never show up in the URL. @@ -162,9 +194,9 @@ export default createMiddleware({ }); ``` -In this case all requests for all locales will be rewritten to have the locale -prefixed internally. You still need to place all your pages inside a -`[locale]` folder for the routes to be able to receive the `locale` param. +In this case, requests for all locales will be rewritten to have the locale only prefixed internally. You still need to place all your pages inside a `[locale]` folder for the routes to be able to receive the `locale` param. + +**Important:** If you use this strategy, you should make sure that [your matcher detects unprefixed pathnames](#matcher-no-prefix). Note that [alternate links](#disable-alternate-links) are disabled in this @@ -205,11 +237,6 @@ export default createMiddleware({ ### Localizing pathnames - - This API is only available in [the Server Components - beta](/docs/getting-started/app-router-server-components). - - Many apps choose to localize pathnames, especially when search engine optimization is relevant, e.g.: - `/en/about` @@ -269,20 +296,30 @@ Because of this, the following config is generally recommended: ```tsx filename="middleware.ts" export const config = { - // Skip all paths that should not be internationalized. This example skips - // certain folders and all pathnames with a dot (e.g. favicon.ico) - matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'] + // Match only internationalized pathnames + matcher: ['/', '/(de|en)/:path*'] }; ``` -However, this can lead to false negatives if you have pages that contain the character `.` (e.g. `/users/jane.doe`). To make sure these are processed by the middleware, you can add corresponding entries to the matcher config: +This enables: + +1. A redirect at `/` to a suitable locale +2. Internationalization of all pathnames starting with a locale (e.g. `/en/about`) + +#### Pathnames without a locale prefix [#matcher-no-prefix] + +If you pick a config for [`localePrefix`](#locale-prefix) other than [`always`](#locale-prefix-always), you need a more flexible matcher since you have to match pathnames without a locale prefix as well (e.g. `/about`). + +A popular strategy is to match all routes that don't start with certain segments (e.g. `/_next`) and also none that include a dot (`.`) since these typically are static files. However, if you have some routes where a dot is expected (e.g. `/users/jane.doe`), you should explicitly provide a matcher for these. ```tsx filename="middleware.ts" export const config = { // Matcher entries are linked with a logical "or", therefore // if one of them matches, the middleware will be invoked. matcher: [ - // Match all pathnames without `.` + // Match all pathnames except for + // - … if they start with `/api`, `/_next` or `/_vercel` + // - … the ones containing a dot (e.g. `favicon.ico`) '/((?!api|_next|_vercel|.*\\..*).*)', // Match all pathnames within `/users`, optionally with a locale prefix '/(.+)?/users/(.+)' @@ -292,7 +329,7 @@ export const config = { {/* Keep this in sync with `packages/next-intl/test/middleware/middleware.test.tsx` */} -Additionally, some third-party providers like [Vercel Analytics](https://vercel.com/analytics) and [umami](https://umami.is/docs/running-on-vercel) typically use internal endpoints that are then rewritten to an external URL (e.g. `/_vercel/insights/view`). Make sure to exclude such requests from your middleware matcher so they aren't accidentally rewritten. +Note that some third-party providers like [Vercel Analytics](https://vercel.com/analytics) and [umami](https://umami.is/docs/running-on-vercel) typically use internal endpoints that are then rewritten to an external URL (e.g. `/_vercel/insights/view`). Make sure to exclude such requests from your middleware matcher so they aren't accidentally rewritten. ## Composing other middlewares @@ -309,30 +346,31 @@ import createIntlMiddleware from 'next-intl/middleware'; import {NextRequest} from 'next/server'; export default async function middleware(request: NextRequest) { - // Step 1: Use the incoming request + // Step 1: Use the incoming request (example) const defaultLocale = request.headers.get('x-default-locale') || 'en'; - // Step 2: Create and call the next-intl middleware + // Step 2: Create and call the next-intl middleware (example) const handleI18nRouting = createIntlMiddleware({ locales: ['en', 'de'], defaultLocale }); const response = handleI18nRouting(request); - // Step 3: Alter the response + // Step 3: Alter the response (example) response.headers.set('x-default-locale', defaultLocale); return response; } export const config = { - matcher: ['/((?!_next|.*\\..*).*)'] + // Match only internationalized pathnames + matcher: ['/', '/(de|en)/:path*'] }; ``` ### Example: Integrating with Clerk -[`@clerk/nextjs`](https://clerk.com/docs/references/nextjs/overview) provides a middleware that can be integrated with `next-intl` by using [the `beforeAuth` hook](https://clerk.com/docs/references/nextjs/auth-middleware#using-before-auth-to-execute-middleware-before-authentication). By doing this, the middleware from `next-intl` will run first, potentially redirect or rewrite incoming requests, followed by the middleware from `@clerk/next` acting on the response. +[`@clerk/nextjs`](https://clerk.com/docs/references/nextjs/overview) provides a middleware that can be integrated with `next-intl` by using the [`beforeAuth` hook](https://clerk.com/docs/references/nextjs/auth-middleware#using-before-auth-to-execute-middleware-before-authentication). By doing this, the middleware from `next-intl` will run first, potentially redirect or rewrite incoming requests, followed by the middleware from `@clerk/next` acting on the response. ```tsx filename="middleware.ts" import {authMiddleware} from '@clerk/nextjs'; @@ -349,11 +387,12 @@ export default authMiddleware({ }, // Ensure that locale-specific sign in pages are public - publicRoutes: ['/', '/:locale/sign-in'] + publicRoutes: ['/:locale', '/:locale/sign-in'] }); export const config = { - matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'] + // Match only internationalized pathnames + matcher: ['/', '/(de|en)/:path*'] }; ``` @@ -373,6 +412,7 @@ const publicPages = ['/', '/login']; const intlMiddleware = createIntlMiddleware({ locales, + localePrefix: 'as-necssary', defaultLocale: 'en' }); @@ -418,27 +458,22 @@ There's a working [example that combines `next-intl` with Auth.js](https://githu -Many thanks to [narakhan](https://github.com/narakhan) for [sharing his middleware implementation](https://github.com/amannn/next-intl/pull/149#issuecomment-1509990635)! - ## Usage without middleware (static export) If you're using the [static export feature from Next.js](https://nextjs.org/docs/app/building-your-application/deploying/static-exports) (`output: 'export'`), the middleware will not run. You can use [prefix-based routing](#prefix-based-routing) nontheless to internationalize your app, but a few tradeoffs apply. **Static export limitations:** -1. There's no default locale that can be used without a prefix (same as [`localePrefix: 'always'`](#always-use-a-locale-prefix)) +1. There's no default locale that can be used without a prefix (same as [`localePrefix: 'always'`](#locale-prefix-always)) 2. The locale can't be negotiated at runtime (same as [`localeDetection: false`](#disable-automatic-locale-detection)) 3. You can't use [pathname localization](#localizing-pathnames) -4. You need to add a redirect for the root of the app -5. Currently this is limited to the usage of `next-intl` in [Client - Components](/docs/getting-started/app-router-client-components) ([Server - Components](/docs/getting-started/app-router-server-components) are not - supported yet). +4. This requires [static rendering](/docs/getting-started/app-router#static-rendering) +5. You need to add a redirect for the root of the app ```tsx filename="app/page.tsx" import {redirect} from 'next/navigation'; -// Redirect the user to the default locale when the app root is requested +// Redirect the user to the default locale when `/` is requested export default function RootPage() { redirect('/en'); } diff --git a/docs/pages/docs/routing/navigation.mdx b/docs/pages/docs/routing/navigation.mdx index 487776b3c..1729d6713 100644 --- a/docs/pages/docs/routing/navigation.mdx +++ b/docs/pages/docs/routing/navigation.mdx @@ -16,7 +16,7 @@ For example: - `/en/about` - `/de/about` -**Localized pathnames:** Many apps choose to localize pathnames however, especially when search engine optimization is relevant. In this case, you'll provide distinct pathnames based on the user locale. +**Localized pathnames:** Many apps choose to localize pathnames, especially when search engine optimization is relevant. In this case, you'll provide distinct pathnames based on the user locale. For example: @@ -25,7 +25,7 @@ For example: --- -Each strategy will provide you with corresponding [navigation APIs](#apis) that you'll typically provide in a central module to easily access them in components (e.g. `./navigation.ts`). +Each strategy will provide you with corresponding [navigation APIs](#apis) that you'll typically provide in a central module to easily access them in components (e.g. `src/navigation.ts`). ### Strategy 1: Shared pathnames [#shared-pathnames] @@ -33,7 +33,7 @@ With this strategy, the pathnames of your app are identical for all locales. Thi To create [navigation APIs](#apis) for this strategy, use the `createSharedPathnamesNavigation` function: -```tsx filename="./navigation.ts" +```tsx filename="navigation.ts" import {createSharedPathnamesNavigation} from 'next-intl/navigation'; export const locales = ['en', 'de'] as const; @@ -60,7 +60,7 @@ When using this strategy, you have to provide distinct pathnames for every local You can use the `createLocalizedPathnamesNavigation` function to create corresponding [navigation APIs](#apis): -```tsx filename="./navigation.ts" +```tsx filename="navigation.ts" import { createLocalizedPathnamesNavigation, Pathnames @@ -291,7 +291,7 @@ The [navigation APIs](#apis) are strictly typed and only allow routes specified … or globally configure `createLocalizedPathnamesNavigation` to accept arbitrary strings too: -```tsx filename="./navigation.ts" +```tsx filename="navigation.ts" // ... export const {Link, redirect, usePathname, useRouter} = @@ -445,11 +445,6 @@ Note that internal pathnames are returned without params being resolved (e.g. `/ ### `redirect` - - This API is only available in [the Server Components - beta](/docs/getting-started/app-router-server-components). - - If you want to interrupt the render and redirect to another page, you can invoke the `redirect` function. This wraps [the `redirect` function from Next.js](https://nextjs.org/docs/app/api-reference/functions/redirect) and automatically applies the current locale. @@ -499,10 +494,7 @@ If you need to construct a particular pathname based on a locale, you can call t - - This API is only available for localized pathnames, since it is not necessary - for shared pathnames. - +(This API is only available for localized pathnames, since it is not necessary for shared pathnames.) diff --git a/docs/pages/docs/usage/_meta.json b/docs/pages/docs/usage/_meta.json index 3d2535fce..b2c496241 100644 --- a/docs/pages/docs/usage/_meta.json +++ b/docs/pages/docs/usage/_meta.json @@ -3,5 +3,6 @@ "messages": "Messages", "numbers": "Numbers", "dates-times": "Dates and times", - "lists": "Lists" + "lists": "Lists", + "configuration": "Global configuration" } \ No newline at end of file diff --git a/docs/pages/docs/configuration.mdx b/docs/pages/docs/usage/configuration.mdx similarity index 73% rename from docs/pages/docs/configuration.mdx rename to docs/pages/docs/usage/configuration.mdx index 8a1920860..0cc3faa87 100644 --- a/docs/pages/docs/configuration.mdx +++ b/docs/pages/docs/usage/configuration.mdx @@ -1,7 +1,6 @@ import PartnerContentLink from 'components/PartnerContentLink'; import Callout from 'components/Callout'; -import VersionTabs from 'components/VersionTabs'; -import {Tab} from 'nextra-theme-docs'; +import {Tab, Tabs} from 'nextra-theme-docs'; # Global configuration @@ -9,10 +8,23 @@ Configuration properties that you use across your Next.js app can be set globall ## Client- and Server Components [#client-server-components] -Depending on if you handle [internationalization in Client- or Server Components](/docs/environments/server-client-components), the configuration from `NextIntlClientProvider` or `i18n.ts` will be applied respectively. +Depending on if you handle [internationalization in Client- or Server Components](/docs/environments/server-client-components), the configuration from `i18n.ts` or `NextIntlClientProvider` will be applied respectively. - - +### `i18n.ts` + +`i18n.ts` can be used to provide configuration for **Server Components**. + +```tsx filename="i18n.ts" +import {getRequestConfig} from 'next-intl/server'; + +export default getRequestConfig(async ({locale}) => ({ + messages: (await import(`../messages/${locale}.json`)).default +})); +``` + +The configuration object is created once for each request by internally using React's [`cache`](https://nextjs.org/docs/app/building-your-application/data-fetching/caching#react-cache). The first component to use internationalization will call the function defined with `getRequestConfig`. + +### `NextIntlClientProvider` `NextIntlClientProvider` can be used to provide configuration for **Client Components**. @@ -31,7 +43,7 @@ export default async function LocaleLayout({children, params: {locale}}) { return ( - + {children} @@ -40,29 +52,9 @@ export default async function LocaleLayout({children, params: {locale}}) { } ``` -Note that `NextIntlClientProvider` inherits the props `locale`, `now` and `timeZone` when the component is rendered from a Server Component. Other configuration like `messages` and `formats` can be provided as necessary. - - - - -
- This only applies if you're using [the Server Components beta](/docs/getting-started/app-router-server-components). -
- -`i18n.ts` can be used to provide configuration for **Server Components**. - -```tsx filename="i18n.ts" -import {getRequestConfig} from 'next-intl/server'; - -export default getRequestConfig(async ({locale}) => ({ - messages: (await import(`./messages/${locale}.json`)).default -})); -``` - -The configuration object is created once for each request by internally using React's [`cache`](https://nextjs.org/docs/app/building-your-application/data-fetching/caching#react-cache). The first component to use internationalization will call the function defined with `getRequestConfig`. - -
-
+ + `NextIntlClientProvider` inherits the props `locale`, `now` and `timeZone` when the component is rendered from a Server Component. Other configuration like `messages` and `formats` can be provided as necessary. + ## Messages @@ -76,52 +68,38 @@ The most crucial aspect of internationalization is providing labels based on the ... ``` - - - -```tsx filename="app/[locale]/layout.tsx" /NextIntlClientProvider/ -import {NextIntlClientProvider} from 'next-intl'; -import {notFound} from 'next/navigation'; - -export default async function LocaleLayout({children, params: {locale}}) { - let messages; - try { - messages = (await import(`../../messages/${locale}.json`)).default; - } catch (error) { - notFound(); - } - - return ( - - - - {children} - - - - ); -} -``` - - + -
- This only applies if you're using [the Server Components beta](/docs/getting-started/app-router-server-components). -
- ```tsx filename="i18n.ts" import {getRequestConfig} from 'next-intl/server'; export default getRequestConfig(async ({locale}) => ({ - messages: (await import(`./messages/${locale}.json`)).default + messages: (await import(`../messages/${locale}.json`)).default })); ```
-
+ -Colocating your messages with app code is beneficial because it allows developers to make changes to messages quickly. Additionally, you can [use the shape of your local messages for type checking](/docs/workflows/typescript). +```tsx +import {NextIntlClientProvider} from 'next-intl'; + +// ... + +const messages = useMessages(); + +return ( + + {children} + +); +``` + + +
+ +Colocating your messages with app code is beneficial because it allows developers to make changes quickly. Additionally, you can [use the shape of your local messages for type checking](/docs/workflows/typescript). Translators can collaborate on messages by using CI tools, such as Crowdin's GitHub integration, which allows changes to be synchronized directly into your code repository. @@ -174,29 +152,11 @@ const messages = deepmerge(defaultMessages, userMessages); ## Time zone -If possible, you should configure an explicit time zone, as this affects the rendering of dates and times. By default, the available time zone of the runtime will be used: In Node.js this is the time zone that is configured for the server and in the browser, this is the local time zone of the user. As the time zone of the server and the one from the user can differ, this can lead to markup mismatches when your app is both rendered on the server as well as on the client side. - -To ensure consistency, you can globally define a time zone: - - - - -```tsx -// The time zone can either be statically defined, read from the -// user profile if you store such a setting, or based on dynamic -// request information like the locale or headers. -const timeZone = 'Europe/Vienna'; - -... -``` +Specifying a time zone affects the rendering of dates and times. By default, the time zone of the server runtime will be used, but can be customized as necessary. - + -
- This only applies if you're using [the Server Components beta](/docs/getting-started/app-router-server-components). -
- ```tsx filename="i18n.ts" import {getRequestConfig} from 'next-intl/server'; @@ -210,35 +170,35 @@ export default getRequestConfig(async ({locale}) => ({ ```
-
+ + +```tsx +// The time zone can either be statically defined, read from the +// user profile if you store such a setting, or based on dynamic +// request information like the locale or headers. +const timeZone = 'Europe/Vienna'; + +... +``` + + +
The available time zone names can be looked up in [the tz database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). + +The time zone 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. For all other cases, you can specify the value explicitly. + + ## Now value [#now] -To avoid mismatches between the server and client environment, it is recommended to configure a global value for `now`: +When formatting [relative dates and times](/docs/usage/dates-times#formatting-relative-time), `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`: -```tsx - - - -``` - - + -
- This only applies if you're using [the Server Components beta](/docs/getting-started/app-router-server-components). -
- ```tsx filename="i18n.ts" import {getRequestConfig} from 'next-intl/server'; @@ -251,9 +211,16 @@ export default getRequestConfig(async ({locale}) => ({ ```
-
+ + +```tsx +const now = new Date('2020-11-20T10:36:01.516Z'); -This value will be used as the default for [the `relativeTime` function](/docs/usage/dates-times#formatting-relative-time) as well as returned during the initial render of [`useNow`](/docs/usage/dates-times#usenow). +... +``` + + + **Tip:** For consistent results in end-to-end tests, it can be helpful to mock this value to a constant value, e.g. based on an environment parameter. @@ -265,17 +232,22 @@ This value will be used as the default for [the `relativeTime` function](/docs/u [`useNow`](/docs/usage/dates-times#usenow) on the client side. + +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. + + ## Formats To achieve consistent date, time, number and list formatting, you can define a set of global formats. - + + +```tsx filename="i18n.ts" +import {getRequestConfig} from 'next-intl/server'; -```tsx - ({ + formats: { dateTime: { short: { day: 'numeric', @@ -294,24 +266,17 @@ To achieve consistent date, time, number and list formatting, you can define a s type: 'conjunction' } } - }} -> - - + }, + // ... +})); ``` - -
- This only applies if you're using [the Server Components beta](/docs/getting-started/app-router-server-components). -
- -```tsx filename="i18n.ts" -import {getRequestConfig} from 'next-intl/server'; -export default getRequestConfig(async ({locale}) => ({ - formats: { +```tsx + ({ type: 'conjunction' } } - }, - // ... -})); + }} +> + ... + ```
-
+ + +Usage in components: ```tsx import {useFormatter} from 'next-intl'; @@ -374,27 +342,8 @@ function Component() { To achieve consistent usage of translation values and reduce redundancy, you can define a set of global default values. This configuration can also be used to apply consistent styling of commonly used rich text elements. - + - -```tsx - {chunks}, - value: 123 - }} -> - - -``` - - - - -
- This only applies if you're using [the Server Components beta](/docs/getting-started/app-router-server-components). -
```tsx filename="i18n.ts" import {getRequestConfig} from 'next-intl/server'; @@ -409,55 +358,33 @@ export default getRequestConfig(async ({locale}) => ({ ```
-
- -The defaults will be overridden by locally provided values. - -## Error handling - -By default, when a message fails to resolve or when the formatting failed, an error will be printed on the console. In this case `${namespace}.${key}` will be rendered instead to keep your app running. - -This behavior can be customized with the `onError` and `getMessageFallback` configuration option. - - ```tsx -import {NextIntlClientProvider, IntlErrorCode} from 'next-intl'; + {chunks}, + value: 123 + }} +> + ... + +``` -function onError(error) { - if (error.code === IntlErrorCode.MISSING_MESSAGE) { - // Missing translations are expected and should only log an error - console.error(error); - } else { - // Other errors indicate a bug in the app and should be reported - reportToErrorTracking(error); - } -} + + -function getMessageFallback({namespace, key, error}) { - const path = [namespace, key].filter((part) => part != null).join('.'); +The defaults will be overridden if local formats are provided at a specific call site. - if (error.code === IntlErrorCode.MISSING_MESSAGE) { - return path + ' is not yet translated'; - } else { - return 'Dear developer, please fix this message: ' + path; - } -} +## Error handling - - - -``` +By default, when a message fails to resolve or when the formatting failed, an error will be printed on the console. In this case `${namespace}.${key}` will be rendered instead to keep your app running. - +This behavior can be customized with the `onError` and `getMessageFallback` configuration option. + -
- This only applies if you're using [the Server Components beta](/docs/getting-started/app-router-server-components). -
- ```tsx filename="i18n.ts" import {getRequestConfig} from 'next-intl/server'; import {IntlErrorCode} from 'next-intl'; @@ -483,11 +410,41 @@ export default getRequestConfig(async ({locale}) => ({ }, // ... })); +``` -```` +
+ + +```tsx +import {NextIntlClientProvider, IntlErrorCode} from 'next-intl'; + +function onError(error) { + if (error.code === IntlErrorCode.MISSING_MESSAGE) { + // Missing translations are expected and should only log an error + console.error(error); + } else { + // Other errors indicate a bug in the app and should be reported + reportToErrorTracking(error); + } +} + +function getMessageFallback({namespace, key, error}) { + const path = [namespace, key].filter((part) => part != null).join('.'); + + if (error.code === IntlErrorCode.MISSING_MESSAGE) { + return path + ' is not yet translated'; + } else { + return 'Dear developer, please fix this message: ' + path; + } +} + + + ... + +``` -
+ ## Locale @@ -530,4 +487,4 @@ function Component() { const timeZone = useTimeZone(); const messages = useMessages(); } -``` \ No newline at end of file +``` diff --git a/docs/pages/docs/usage/dates-times.mdx b/docs/pages/docs/usage/dates-times.mdx index 769b2ff81..d77e47917 100644 --- a/docs/pages/docs/usage/dates-times.mdx +++ b/docs/pages/docs/usage/dates-times.mdx @@ -5,12 +5,6 @@ import PartnerContentLink from 'components/PartnerContentLink'; The formatting of dates and times varies greatly between locales (e.g. "Apr 24, 2023" in `en-US` vs. "24 квіт. 2023 р." in `uk-UA`). By using the formatting capabilities of `next-intl`, you can handle all i18n differences in your Next.js app automatically. - - -If you're formatting dates and times, you should [set up a global time zone](/docs/configuration#time-zone). - - - ## Formatting dates and times You can format plain dates that are not part of a message with the `useFormatter` hook: @@ -39,10 +33,10 @@ See [the MDN docs about `DateTimeFormat`](https://developer.mozilla.org/en-US/do
How can I parse dates or manipulate them? -To parse dates, you can pass them to [the `Date` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date). - Since `next-intl` is only concerned with formatting dates, you can use a library like [date-fns](https://date-fns.org/) to manipulate them. +To parse dates, you can pass them to [the `Date` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date). + ```tsx import {subDays} from 'date-fns'; @@ -74,7 +68,7 @@ function Component() { Note that values are rounded, so e.g. if 100 seconds have passed, "2 minutes ago" will be returned. -Supplying `now` is necessary for the function to return consistent results. If you have [configured a global value for `now` on the provider](/docs/configuration#global-now-value), you can omit the second argument: +Supplying `now` is necessary for the function to return consistent results. If you have [configured a global value for `now`](/docs/usage/configuration#now), you can omit the second argument: ```js format.relativeTime(dateTime); @@ -177,5 +171,5 @@ t( To reuse date and time formats for multiple components, you can configure - [global formats](/docs/configuration#global-formats). + [global formats](/docs/usage/configuration#formats). diff --git a/docs/pages/docs/usage/index.mdx b/docs/pages/docs/usage/index.mdx index d4e852698..7a0c1d3b2 100644 --- a/docs/pages/docs/usage/index.mdx +++ b/docs/pages/docs/usage/index.mdx @@ -23,4 +23,3 @@ If you prefer a more hands-on approach to learning, you can alternatively explor - [App Router example](/examples/app-router) - [Pages Router example](/examples/pages-router) -- [Pages Router (advanced) example](/examples/pages-router-advanced) diff --git a/docs/pages/docs/usage/lists.mdx b/docs/pages/docs/usage/lists.mdx index 6cc84f641..48e0fe30b 100644 --- a/docs/pages/docs/usage/lists.mdx +++ b/docs/pages/docs/usage/lists.mdx @@ -32,7 +32,7 @@ Note that lists can can currently only be formatted via `useFormatter`, there's To reuse list formats for multiple components, you can configure [global - formats](/docs/configuration#global-formats). + formats](/docs/usage/configuration#formats).
diff --git a/docs/pages/docs/usage/messages.mdx b/docs/pages/docs/usage/messages.mdx index d080d2987..c7d623d7f 100644 --- a/docs/pages/docs/usage/messages.mdx +++ b/docs/pages/docs/usage/messages.mdx @@ -7,14 +7,14 @@ The main part of handling internationalization (typically referred to as _i18n_) ## Terminology -- **Locale**: We use this term to describe an identifier that contains the language and formatting preferences of users. Apart from the language, this includes optional regional information (e.g. `en-US`, `de`). -- **Messages**: These are collections of namespace-label pairs that provide grouping by locale (e.g. `en-US.json`, `de.json`). +- **Locale**: We use this term to describe an identifier that contains the language and formatting preferences of users. Apart from the language, this includes optional regional information (e.g. `en-US`). +- **Messages**: These are collections of namespace-label pairs that provide grouping by locale (e.g. `en-US.json`). ## Structuring messages To group your messages within a locale, it's recommended to use component names as namespaces and embrace them as the primary unit of code organization in your app. -```js filename="en.json" +```json filename="en.json" { "About": { "title": "About us" @@ -46,7 +46,7 @@ t('About.title'); Optionally, you can structure your messages as nested objects. -```js filename="en.json" +```json filename="en.json" { "auth": { "SignUp": { @@ -117,7 +117,7 @@ export default function useLocaleLabel() {
How can I use translations outside of components? -`next-intl` only offers an API to use translations from within React components. +`next-intl` is heavily based on the `useTranslations` API which is intended to consume translations from within React components. This may seem like an unnecessary limitation, but this is intentional and aims to encourage the use of proven patterns that avoid potential issues—especially if they are easy to overlook. @@ -139,7 +139,7 @@ To work with ICU messages, it can be helpful to use an editor that supports this ### Static messages -```js filename="en.json" +```json filename="en.json" "message": "Hello world!" ``` @@ -149,7 +149,7 @@ t('message'); // "Hello world!" ### Interpolation of dynamic values -```js filename="en.json" +```json filename="en.json" "message": "Hello {name}!" ``` @@ -159,7 +159,7 @@ t('message', {name: 'Jane'}); // "Hello Jane!" ### Pluralization -```js filename="en.json" +```json filename="en.json" "message": "You have {count, plural, =0 {no followers yet} =1 {one follower} other {# followers}}." ``` @@ -169,7 +169,7 @@ t('message', {count: 3580}); // "You have 3,580 followers." ### Ordinal pluralization -```js filename="en.json" +```json filename="en.json" "message": "It's your {year, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} birthday!" ``` @@ -179,7 +179,7 @@ t('message', {year: 21}); // "It's your 21st birthday!" ### Selecting enum-based values -```js filename="en.json" +```json filename="en.json" "message": "{gender, select, female {She} male {He} other {They}} is online." ``` @@ -189,7 +189,7 @@ t('message', {gender: 'female'}); // "She is online." ### Escaping -```js filename="en.json" +```json filename="en.json" "message": "Escape curly braces with single quotes (e.g. '{name'})" ``` @@ -201,7 +201,7 @@ t('message'); // "Escape curly braces with single quotes (e.g. {name})" You can format rich text with custom tags and map them to React components. -```js filename="en.json" +```json filename="en.json" { "richText": "This is very important" } @@ -214,7 +214,7 @@ t.rich('richText', { }); ``` -If you want to use the same tag across your app, you can configure it via the [default translation values](/docs/configuration#default-translation-values). +If you want to use the same tag across your app, you can configure it via the [default translation values](/docs/usage/configuration#default-translation-values). Note that the ICU parser doesn't support self-closing tags at this point, therefore you have to use syntax like `

` if your rich text function doesn't intend to receive any `chunks` (e.g. `br: () =>
`). @@ -222,7 +222,7 @@ Note that the ICU parser doesn't support self-closing tags at this point, theref If you need to render a list of messages, the recommended approach is to map an array of keys to the corresponding messages. -```js filename="en.json" +```json filename="en.json" { "Benefits": { "zero-config": "Works with zero config", @@ -251,7 +251,7 @@ function Benefits() { If the number of items varies between locales, you can solve this by using [rich text](#rich-text). -```js filename="en.json" +```json filename="en.json" { "Benefits": { "items": "Works with zero configEasy to customizeBlazingly fast" @@ -279,7 +279,7 @@ function Benefits() { The advantage of this approach over supporting arrays in messages is that this way you can use the formatting capabilities, e.g. to interpolate values into individual messages. -```js filename="en.json" +```json filename="en.json" { "Benefits": { "zero-config": "Works with zero config", @@ -315,7 +315,7 @@ function Benefits() { Messages are always parsed and therefore e.g. for rich text you need to supply the necessary tags. If you want to avoid the parsing, e.g. because you have raw HTML stored in a message, there's a separate API for this use case. -```js filename="en.json" +```json filename="en.json" { "content": "

Headline

This is raw HTML

" } diff --git a/docs/pages/docs/usage/numbers.mdx b/docs/pages/docs/usage/numbers.mdx index 174a693fc..0c0f74600 100644 --- a/docs/pages/docs/usage/numbers.mdx +++ b/docs/pages/docs/usage/numbers.mdx @@ -77,6 +77,6 @@ t( -To reuse number formats for multiple components, you can configure [global formats](/docs/configuration#global-formats). +To reuse number formats for multiple components, you can configure [global formats](/docs/usage/configuration#formats). diff --git a/docs/pages/docs/workflows/typescript.mdx b/docs/pages/docs/workflows/typescript.mdx index aa4d3b1a9..9e6b13dbc 100644 --- a/docs/pages/docs/workflows/typescript.mdx +++ b/docs/pages/docs/workflows/typescript.mdx @@ -35,17 +35,10 @@ declare interface IntlMessages extends Messages {} You can freely define the interface, but if you have your messages available locally, it can be helpful to automatically create the interface based on a messages sample by importing it. - - -
    -
  1. Your interface is called `IntlMessages`.
  2. -
  3. You're using TypeScript version 4 or later.
  4. -
  5. The path of your `import` is correct.
  6. -
  7. Your type declaration file is included in `tsconfig.json`.
  8. -
  9. - Your editor has loaded the most recent type declarations. When in doubt, you - can restart. -
  10. -
- -
+**If you're encountering problems, please double check that:** + +1. Your interface is called `IntlMessages`. +2. You're using TypeScript version 4 or later. +3. The path of your `import` is correct. +4. Your type declaration file is included in `tsconfig.json`. +5. Your editor has loaded the most recent type declarations. When in doubt, you can restart. diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx index cda0c9e28..175c7cbf9 100644 --- a/docs/pages/index.mdx +++ b/docs/pages/index.mdx @@ -21,7 +21,7 @@ import UserTestimonial from 'components/UserTestimonial'; description="Support multiple languages, with your app code becoming simpler instead of more complex." getStarted="Get started" viewExample="View an example" - rscAnnouncement="Support for Next.js 13 and the App Router has arrived →" + rscAnnouncement="Support for Server Components has arrived →" /> @@ -75,7 +75,7 @@ import UserTestimonial from 'components/UserTestimonial'; diff --git a/docs/theme.config.js b/docs/theme.config.js index c0a129c1d..3727aec68 100644 --- a/docs/theme.config.js +++ b/docs/theme.config.js @@ -79,6 +79,20 @@ export default { titleTemplate: '%s – Internationalization (i18n) for Next.js' }; }, + banner: { + text: ( + <> + 3.0 preview (see{' '} + + announcement + + ) + + ) + }, primaryHue: {light: 210, dark: 195}, footer: { component: Footer diff --git a/examples/example-next-13-advanced/src/app/[locale]/client/ClientContent.tsx b/examples/example-next-13-advanced/src/app/[locale]/client/ClientContent.tsx index 6c377fcbb..c3c90ffb2 100644 --- a/examples/example-next-13-advanced/src/app/[locale]/client/ClientContent.tsx +++ b/examples/example-next-13-advanced/src/app/[locale]/client/ClientContent.tsx @@ -1,8 +1,7 @@ 'use client'; import {useNow} from 'next-intl'; -import {usePathname} from 'next-intl/client'; -import {Link} from '../../../navigation'; +import {Link, usePathname} from '../../../navigation'; export default function ClientContent() { const now = useNow(); diff --git a/examples/example-next-13-advanced/src/components/CoreLibrary.tsx b/examples/example-next-13-advanced/src/components/CoreLibrary.tsx index 958fa020c..eda90ff30 100644 --- a/examples/example-next-13-advanced/src/components/CoreLibrary.tsx +++ b/examples/example-next-13-advanced/src/components/CoreLibrary.tsx @@ -1,4 +1,4 @@ -import {createTranslator, createIntl} from 'next-intl'; +import {createTranslator, createFormatter} from 'next-intl'; export default function CoreLibrary() { const t = createTranslator({ @@ -7,12 +7,12 @@ export default function CoreLibrary() { }); const now = new Date(2022, 10, 6, 20, 20, 0, 0); - const intl = createIntl({locale: 'en', now}); + const format = createFormatter({locale: 'en', now}); const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000); return (

- {t('Index.title')} {intl.formatRelativeTime(tomorrow)} + {t('Index.title')} {format.relativeTime(tomorrow)}

); } diff --git a/examples/example-next-13-advanced/src/middleware.ts b/examples/example-next-13-advanced/src/middleware.ts index b5529e52b..4006e7a92 100644 --- a/examples/example-next-13-advanced/src/middleware.ts +++ b/examples/example-next-13-advanced/src/middleware.ts @@ -3,6 +3,7 @@ import {locales, pathnames} from './navigation'; export default createMiddleware({ defaultLocale: 'en', + localePrefix: 'as-needed', pathnames, locales }); diff --git a/examples/example-next-13-next-auth/src/components/LocaleSwitcher.tsx b/examples/example-next-13-next-auth/src/components/LocaleSwitcher.tsx index b36e8d373..3e31beb45 100644 --- a/examples/example-next-13-next-auth/src/components/LocaleSwitcher.tsx +++ b/examples/example-next-13-next-auth/src/components/LocaleSwitcher.tsx @@ -1,6 +1,5 @@ import {useLocale, useTranslations} from 'next-intl'; -import {usePathname} from 'next-intl/client'; -import Link from 'next-intl/link'; +import {Link, usePathname} from '../navigation'; export default function LocaleSwitcher() { const t = useTranslations('LocaleSwitcher'); diff --git a/examples/example-next-13-next-auth/src/middleware.tsx b/examples/example-next-13-next-auth/src/middleware.tsx index 21c8605fb..fb6202ee5 100644 --- a/examples/example-next-13-next-auth/src/middleware.tsx +++ b/examples/example-next-13-next-auth/src/middleware.tsx @@ -1,8 +1,8 @@ import {NextRequest} from 'next/server'; import {withAuth} from 'next-auth/middleware'; import createIntlMiddleware from 'next-intl/middleware'; +import {locales} from './navigation'; -const locales = ['en', 'de']; const publicPages = [ '/', '/login' @@ -11,6 +11,7 @@ const publicPages = [ const intlMiddleware = createIntlMiddleware({ locales, + localePrefix: 'as-needed', defaultLocale: 'en' }); diff --git a/examples/example-next-13-next-auth/src/navigation.ts b/examples/example-next-13-next-auth/src/navigation.ts new file mode 100644 index 000000000..aa71dd13d --- /dev/null +++ b/examples/example-next-13-next-auth/src/navigation.ts @@ -0,0 +1,5 @@ +import {createSharedPathnamesNavigation} from 'next-intl/navigation'; + +export const locales = ['en', 'de'] as const; +export const {Link, redirect, usePathname, useRouter} = + createSharedPathnamesNavigation({locales}); diff --git a/examples/example-next-13-with-pages/src/app/[locale]/page.tsx b/examples/example-next-13-with-pages/src/app/[locale]/page.tsx index aa0154f71..131116dc4 100644 --- a/examples/example-next-13-with-pages/src/app/[locale]/page.tsx +++ b/examples/example-next-13-with-pages/src/app/[locale]/page.tsx @@ -1,7 +1,7 @@ import {useLocale, useTranslations} from 'next-intl'; -import Link from 'next-intl/link'; import LocaleSwitcher from '../../components/LocaleSwitcher'; import PageLayout from '../../components/PageLayout'; +import {Link} from '../../navigation'; export default function Index() { const t = useTranslations('Index'); @@ -12,7 +12,7 @@ export default function Index() {

{t('description')}

- + {t('navigateToAbout')}

diff --git a/examples/example-next-13-with-pages/src/middleware.ts b/examples/example-next-13-with-pages/src/middleware.ts index 8675074bb..76a4688cc 100644 --- a/examples/example-next-13-with-pages/src/middleware.ts +++ b/examples/example-next-13-with-pages/src/middleware.ts @@ -1,7 +1,8 @@ import createMiddleware from 'next-intl/middleware'; +import {locales} from './navigation'; export default createMiddleware({ - locales: ['en', 'de'], + locales, defaultLocale: 'en' }); diff --git a/examples/example-next-13-with-pages/src/navigation.ts b/examples/example-next-13-with-pages/src/navigation.ts new file mode 100644 index 000000000..aa71dd13d --- /dev/null +++ b/examples/example-next-13-with-pages/src/navigation.ts @@ -0,0 +1,5 @@ +import {createSharedPathnamesNavigation} from 'next-intl/navigation'; + +export const locales = ['en', 'de'] as const; +export const {Link, redirect, usePathname, useRouter} = + createSharedPathnamesNavigation({locales}); diff --git a/examples/example-next-13-with-pages/src/pages/_app.tsx b/examples/example-next-13-with-pages/src/pages/_app.tsx index 70f6ad26b..30ecd482b 100644 --- a/examples/example-next-13-with-pages/src/pages/_app.tsx +++ b/examples/example-next-13-with-pages/src/pages/_app.tsx @@ -1,6 +1,6 @@ import {AppProps} from 'next/app'; import {NextRouter, withRouter} from 'next/router'; -import {NextIntlProvider} from 'next-intl'; +import {NextIntlClientProvider} from 'next-intl'; type Props = AppProps & { router: NextRouter; @@ -8,12 +8,12 @@ type Props = AppProps & { function App({Component, pageProps, router}: Props) { return ( - - + ); } diff --git a/examples/example-next-13/messages/de.json b/examples/example-next-13/messages/de.json index ca70e44db..f28ef9fae 100644 --- a/examples/example-next-13/messages/de.json +++ b/examples/example-next-13/messages/de.json @@ -5,7 +5,7 @@ }, "AboutPage": { "title": "Über", - "description": "

Auch das Routing ist internationalisiert.

Wenn du die Standardsprache Englisch verwendest, siehst du /about in der Adressleiste des Browsers auf dieser Seite.

Wenn du die Sprache auf Deutsch änderst, wird die URL mit der Locale ergänzt und lokalisiert (/de/über).

" + "description": "

Auch das Routing ist internationalisiert.

Wenn du die Standardsprache Englisch verwendest, siehst du /en/about in der Adressleiste des Browsers auf dieser Seite.

Wenn du die Sprache auf Deutsch änderst, wird die URL mit der entsprechend lokalisiert (/de/über).

" }, "Error": { "title": "Etwas ist schief gelaufen!", diff --git a/examples/example-next-13/messages/en.json b/examples/example-next-13/messages/en.json index f7260daf0..ef8dbb2a5 100644 --- a/examples/example-next-13/messages/en.json +++ b/examples/example-next-13/messages/en.json @@ -5,7 +5,7 @@ }, "AboutPage": { "title": "About", - "description": "

The routing is internationalized too.

If you're using the default language English, you'll see /about in the browser address bar on this page.

If you change the locale to German, the URL is prefixed with the locale and localized accordingly (/de/über).

" + "description": "

The routing is internationalized too.

If you're using the default language English, you'll see /en/about in the browser address bar on this page.

If you change the locale to German, the URL is localized accordingly (/de/über).

" }, "Error": { "title": "Something went wrong!", diff --git a/examples/example-next-13/src/app/[locale]/about/page.tsx b/examples/example-next-13/src/app/[locale]/about/page.tsx index eb6b20252..ffb97df44 100644 --- a/examples/example-next-13/src/app/[locale]/about/page.tsx +++ b/examples/example-next-13/src/app/[locale]/about/page.tsx @@ -12,7 +12,7 @@ export default function AboutPage({params: {locale}}: Props) { return ( -
+
{t.rich('description', { p: (chunks) =>

{chunks}

, code: (chunks) => ( diff --git a/examples/example-next-13/src/app/[locale]/layout.tsx b/examples/example-next-13/src/app/[locale]/layout.tsx index 8071055c7..63d9a2d1e 100644 --- a/examples/example-next-13/src/app/[locale]/layout.tsx +++ b/examples/example-next-13/src/app/[locale]/layout.tsx @@ -32,8 +32,7 @@ export default async function LocaleLayout({ params: {locale} }: Props) { // Validate that the incoming `locale` parameter is valid - const isValidLocale = locales.some((cur) => cur === locale); - if (!isValidLocale) notFound(); + if (!locales.includes(locale as any)) notFound(); unstable_setRequestLocale(locale); return ( diff --git a/examples/example-next-13/src/middleware.ts b/examples/example-next-13/src/middleware.ts index dde608338..b3627145a 100644 --- a/examples/example-next-13/src/middleware.ts +++ b/examples/example-next-13/src/middleware.ts @@ -8,6 +8,6 @@ export default createMiddleware({ }); export const config = { - // Skip all paths that should not be internationalized - matcher: ['/((?!api|_next|.*\\..*).*)'] + // Match only internationalized pathnames + matcher: ['/', '/(de|en)/:path*'] }; diff --git a/examples/example-next-13/tests/main.spec.ts b/examples/example-next-13/tests/main.spec.ts index 523eae149..2cd9e7b7c 100644 --- a/examples/example-next-13/tests/main.spec.ts +++ b/examples/example-next-13/tests/main.spec.ts @@ -2,7 +2,7 @@ import {test as it, expect} from '@playwright/test'; it('handles i18n routing', async ({page}) => { await page.goto('/'); - await expect(page).toHaveURL('/'); + await expect(page).toHaveURL('/en'); // A cookie remembers the last locale await page.goto('/de'); @@ -12,7 +12,7 @@ it('handles i18n routing', async ({page}) => { .getByRole('combobox', {name: 'Sprache ändern'}) .selectOption({label: 'Englisch'}); - await expect(page).toHaveURL('/'); + await expect(page).toHaveURL('/en'); page.getByRole('heading', {name: 'next-intl example'}); }); @@ -34,7 +34,7 @@ it("handles not found pages for routes that don't match the middleware", async ( }); it('sets caching headers', async ({request}) => { - for (const pathname of ['/', '/about', '/de', '/de/ueber']) { + for (const pathname of ['/en', '/en/about', '/de', '/de/ueber']) { expect((await request.get(pathname)).headers()['cache-control']).toBe( 's-maxage=31536000, stale-while-revalidate' ); @@ -58,7 +58,7 @@ it('can be used to localize the page', async ({page}) => { }); it('sets a cookie', async ({page}) => { - const response = await page.goto('/'); + const response = await page.goto('/en'); const value = await response?.headerValue('set-cookie'); expect(value).toContain('NEXT_LOCALE=en;'); expect(value).toContain('Path=/;'); diff --git a/packages/next-intl/link.d.ts b/packages/next-intl/link.d.ts deleted file mode 100644 index ea51c91da..000000000 --- a/packages/next-intl/link.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Link from './dist/types/src/link'; - -export = Link; diff --git a/packages/next-intl/package.json b/packages/next-intl/package.json index 0e344793e..496a20339 100644 --- a/packages/next-intl/package.json +++ b/packages/next-intl/package.json @@ -32,15 +32,6 @@ "react-server": "./dist/esm/server.react-server.js", "default": "./dist/server.js" }, - "./client": { - "types": "./client.d.ts", - "default": "./dist/client.js" - }, - "./link": { - "types": "./link.d.ts", - "react-server": "./dist/esm/link.react-server.js", - "default": "./dist/link.js" - }, "./config": { "types": "./config.d.ts", "default": "./dist/config.js" @@ -54,7 +45,6 @@ "react-server": "./dist/esm/navigation.react-server.js", "default": "./dist/navigation.js" }, - "./withNextIntl": "./withNextIntl.js", "./plugin": { "types": "./plugin.d.ts", "default": "./dist/plugin.js" @@ -63,12 +53,8 @@ "files": [ "dist", "server.d.ts", - "client.d.ts", "navigation.d.ts", - "link.d.ts", "middleware.d.ts", - "withNextIntl.js", - "withNextIntl.d.ts", "plugin.d.ts", "config.d.ts" ], @@ -118,24 +104,16 @@ "size-limit": [ { "path": "dist/production/index.js", - "limit": "13.65 KB" + "limit": "12.7 KB" }, { "path": "dist/production/navigation.js", - "limit": "2.5 KB" + "limit": "2.6 KB" }, { "path": "dist/production/server.js", "limit": "1.3 KB" }, - { - "path": "dist/production/client.js", - "limit": "1.8 KB" - }, - { - "path": "dist/production/link.js", - "limit": "1.4 KB" - }, { "path": "dist/production/middleware.js", "limit": "5.9 KB" diff --git a/packages/next-intl/rollup.config.js b/packages/next-intl/rollup.config.js index 3268cfe60..1e9f90dff 100644 --- a/packages/next-intl/rollup.config.js +++ b/packages/next-intl/rollup.config.js @@ -7,16 +7,12 @@ const config = { index: 'src/index.tsx', 'index.react-server': 'src/index.react-server.tsx', - link: 'src/link.tsx', - 'link.react-server': 'src/link.react-server.tsx', - navigation: 'src/navigation.tsx', 'navigation.react-server': 'src/navigation.react-server.tsx', server: 'src/server.tsx', 'server.react-server': 'src/server.react-server.tsx', - client: 'src/client.tsx', middleware: 'src/middleware.tsx', plugin: 'src/plugin.tsx', config: 'src/config.tsx' diff --git a/packages/next-intl/src/client.tsx b/packages/next-intl/src/client.tsx deleted file mode 100644 index 40a7340d3..000000000 --- a/packages/next-intl/src/client.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './client/index'; diff --git a/packages/next-intl/src/client/NextIntlClientProvider.tsx b/packages/next-intl/src/client/NextIntlClientProvider.tsx deleted file mode 100644 index d63924fff..000000000 --- a/packages/next-intl/src/client/NextIntlClientProvider.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React, {ComponentProps} from 'react'; -import NextIntlClientProvider_ from '../shared/NextIntlClientProvider'; - -let hasWarned = false; -/** @deprecated Should be imported from `next-intl`, not `next-intl/client`. */ -export default function NextIntlClientProvider( - props: ComponentProps -) { - if (process.env.NODE_ENV !== 'production' && !hasWarned) { - hasWarned = true; - console.warn(` -Importing \`NextIntlClientProvider\` from \`next-intl/client\` is deprecated. Please update the import: - - import {NextIntlClientProvider} from 'next-intl'; -`); - } - return ; -} diff --git a/packages/next-intl/src/client/index.tsx b/packages/next-intl/src/client/index.tsx deleted file mode 100644 index f4f00b20d..000000000 --- a/packages/next-intl/src/client/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Client-only APIs available via `next-intl/client`. - */ - -import usePathname from './usePathname'; -import useRouter from './useRouter'; - -export {default as useRouter} from './useRouter'; -export {default as usePathname} from './usePathname'; - -/** @deprecated Is called `usePathname` now. */ -export const useUnlocalizedPathname = usePathname; - -/** @deprecated Is called `useRouter` now. */ -export const useLocalizedRouter = useRouter; - -// Legacy export (deprecation is handled by component) -export {default as NextIntlClientProvider} from './NextIntlClientProvider'; diff --git a/packages/next-intl/src/link.react-server.tsx b/packages/next-intl/src/link.react-server.tsx deleted file mode 100644 index a267c5953..000000000 --- a/packages/next-intl/src/link.react-server.tsx +++ /dev/null @@ -1 +0,0 @@ -export {default} from './link/react-server'; diff --git a/packages/next-intl/src/link.tsx b/packages/next-intl/src/link.tsx deleted file mode 100644 index 3bb1ffca8..000000000 --- a/packages/next-intl/src/link.tsx +++ /dev/null @@ -1 +0,0 @@ -export {default} from './link/index'; diff --git a/packages/next-intl/src/link/index.tsx b/packages/next-intl/src/link/index.tsx deleted file mode 100644 index 1ea5f2a75..000000000 --- a/packages/next-intl/src/link/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export {default} from './Link'; diff --git a/packages/next-intl/src/link/react-server/index.tsx b/packages/next-intl/src/link/react-server/index.tsx deleted file mode 100644 index 1ea5f2a75..000000000 --- a/packages/next-intl/src/link/react-server/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export {default} from './Link'; diff --git a/packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx b/packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx index 3025892af..1b22f03d7 100644 --- a/packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx +++ b/packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx @@ -1,20 +1,5 @@ import {AllLocales, Pathnames} from '../shared/types'; -type RoutingConfigPrefix = { - type: 'prefix'; - - /** The default locale can be used without a prefix (e.g. `/about`). If you prefer to have a prefix for the default locale as well (e.g. `/en/about`), you can switch this option to `always`. - */ - prefix?: 'as-needed' | 'always'; -}; - -type RoutingConfigDomain = { - type: 'domain'; - - /** Provide a list of mappings between domains and locales. Note that the `x-forwarded-host` or alternatively the `host` header will be used to determine the requested domain. */ - domains: Array<{domain: string; locale: string}>; -}; - type LocalePrefix = 'as-needed' | 'always' | 'never'; type RoutingBaseConfig = { @@ -38,9 +23,6 @@ export type DomainConfig = Omit< /** The locales availabe on this particular domain. */ locales?: RoutingBaseConfig>['locales']; - - /** @deprecated Use `defaultLocale` instead. */ - locale?: string; }; type MiddlewareConfig = @@ -51,9 +33,6 @@ type MiddlewareConfig = /** Sets the `Link` response header to notify search engines about content in other languages (defaults to `true`). See https://developers.google.com/search/docs/specialty/international/localized-versions#http */ alternateLinks?: boolean; - /** @deprecated Deprecated in favor of `localePrefix` and `domains`. */ - routing?: RoutingConfigPrefix | RoutingConfigDomain; - /** By setting this to `false`, the `accept-language` header will no longer be used for locale detection. */ localeDetection?: boolean; diff --git a/packages/next-intl/src/middleware/middleware.tsx b/packages/next-intl/src/middleware/middleware.tsx index 455f5069a..693258868 100644 --- a/packages/next-intl/src/middleware/middleware.tsx +++ b/packages/next-intl/src/middleware/middleware.tsx @@ -20,63 +20,13 @@ import { const ROOT_URL = '/'; -function handleConfigDeprecations( - config: MiddlewareConfig -) { - if (config.routing) { - const {routing} = config; - config = {...config}; - delete config.routing; - - if (routing.type === 'prefix') { - config.localePrefix = routing.prefix; - } else if (routing.type === 'domain') { - config.domains = routing.domains.map((cur) => ({ - domain: cur.domain, - defaultLocale: cur.locale, - locales: [cur.locale] - })); - } - - if (process.env.NODE_ENV !== 'production') { - console.error( - "\n\nThe `routing` option is deprecated, please use `localePrefix` and `domains` instead. Here's your updated configuration:\n\n" + - JSON.stringify(config, null, 2) + - '\n\nThank you so much for following along with the Server Components beta and sorry for the inconvenience!\n\n' - ); - } - } - - if (config.domains) { - const {domains} = config; - config = {...config}; - config.domains = domains.map((cur) => { - if (process.env.NODE_ENV !== 'production' && cur.locale) { - console.error( - '\n\nThe `domain.locale` option is deprecated, please use `domain.defaultLocale` instead.' - ); - } - return { - ...cur, - defaultLocale: cur.locale || cur.defaultLocale, - ...(cur.locale && {locales: [cur.locale]}) - }; - }); - } - - return config; -} - function receiveConfig( config: MiddlewareConfig ): MiddlewareConfigWithDefaults { - // TODO: Remove before stable release - config = handleConfigDeprecations(config); - const result: MiddlewareConfigWithDefaults = { ...config, alternateLinks: config.alternateLinks ?? true, - localePrefix: config.localePrefix ?? 'as-needed', + localePrefix: config.localePrefix ?? 'always', localeDetection: config.localeDetection ?? true }; @@ -88,17 +38,7 @@ export default function createMiddleware( ) { const configWithDefaults = receiveConfig(config); - // Currently only in use to enable a seamless upgrade path from the - // `{createIntlMiddleware} from 'next-intl/server'` API. - // TODO: Remove in next major release. - const matcher: Array | undefined = (config as any)._matcher; - return function middleware(request: NextRequest) { - const matches = - !matcher || - matcher.some((pattern) => request.nextUrl.pathname.match(pattern)); - if (!matches) return NextResponse.next(); - const {domain, locale} = resolveLocale( configWithDefaults, request.headers, diff --git a/packages/next-intl/src/link/Link.tsx b/packages/next-intl/src/navigation/BaseLink.tsx similarity index 73% rename from packages/next-intl/src/link/Link.tsx rename to packages/next-intl/src/navigation/BaseLink.tsx index 6800dbdbe..bb703102f 100644 --- a/packages/next-intl/src/link/Link.tsx +++ b/packages/next-intl/src/navigation/BaseLink.tsx @@ -1,21 +1,23 @@ import React, {ComponentProps, ReactElement, forwardRef} from 'react'; import useLocale from '../react-client/useLocale'; -import BaseLink from '../shared/BaseLink'; +import BaseLinkWithLocale from '../shared/BaseLinkWithLocale'; import {AllLocales} from '../shared/types'; type Props = Omit< - ComponentProps, + ComponentProps, 'locale' > & { locale?: Locales[number]; }; -function Link( +function BaseLink( {locale, ...rest}: Props, ref: Props['ref'] ) { const defaultLocale = useLocale(); - return ; + return ( + + ); } /** @@ -38,8 +40,8 @@ function Link( * the `set-cookie` response header would cause the locale cookie on the current * page to be overwritten before the user even decides to change the locale. */ -const LinkWithRef = forwardRef(Link) as ( +const BaseLinkWithRef = forwardRef(BaseLink) as ( props: Props & {ref?: Props['ref']} ) => ReactElement; -(LinkWithRef as any).displayName = 'Link'; -export default LinkWithRef; +(BaseLinkWithRef as any).displayName = 'Link'; +export default BaseLinkWithRef; diff --git a/packages/next-intl/src/server/react-client/redirect.tsx b/packages/next-intl/src/navigation/baseRedirect.tsx similarity index 58% rename from packages/next-intl/src/server/react-client/redirect.tsx rename to packages/next-intl/src/navigation/baseRedirect.tsx index b3dbf7d66..05c739244 100644 --- a/packages/next-intl/src/server/react-client/redirect.tsx +++ b/packages/next-intl/src/navigation/baseRedirect.tsx @@ -1,10 +1,10 @@ -import useLocale from '../../react-client/useLocale'; -import {ParametersExceptFirstTwo} from '../../shared/types'; -import baseRedirect from '../baseRedirect'; +import useLocale from '../react-client/useLocale'; +import redirectWithLocale from '../shared/redirectWithLocale'; +import {ParametersExceptFirstTwo} from '../shared/types'; -export default function redirect( +export default function baseRedirect( pathname: string, - ...args: ParametersExceptFirstTwo + ...args: ParametersExceptFirstTwo ) { let locale; try { @@ -18,5 +18,5 @@ export default function redirect( ); } - return baseRedirect(pathname, locale, ...args); + return redirectWithLocale(pathname, locale, ...args); } diff --git a/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx b/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx index e9123675e..5d35ec9a5 100644 --- a/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx +++ b/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx @@ -1,12 +1,10 @@ import React, {ComponentProps, ReactElement, forwardRef} from 'react'; -import { - useRouter as useBaseRouter, - usePathname as useBasePathname -} from '../client'; -import BaseLink from '../link'; import useLocale from '../react-client/useLocale'; -import baseRedirect from '../server/react-client/redirect'; import {AllLocales, ParametersExceptFirst, Pathnames} from '../shared/types'; +import BaseLink from './BaseLink'; +import baseRedirect from './baseRedirect'; +import useBasePathname from './useBasePathname'; +import useBaseRouter from './useBaseRouter'; import { compileLocalizedPathname, getRoute, @@ -19,8 +17,17 @@ export default function createLocalizedPathnamesNavigation< Locales extends AllLocales, PathnamesConfig extends Pathnames >({locales, pathnames}: {locales: Locales; pathnames: PathnamesConfig}) { - function useTypedLocale() { - return useLocale() as (typeof locales)[number]; + function useTypedLocale(): (typeof locales)[number] { + const locale = useLocale(); + const isValid = locales.includes(locale as any); + if (!isValid) { + throw new Error( + process.env.NODE_ENV !== 'production' + ? `Unknown locale encountered: "${locale}". Make sure to validate the locale in \`app/[locale]/layout.tsx\`.` + : undefined + ); + } + return locale; } type LinkProps = Omit< diff --git a/packages/next-intl/src/navigation/createSharedPathnamesNavigation.tsx b/packages/next-intl/src/navigation/createSharedPathnamesNavigation.tsx index e685fdb93..4ca8a2150 100644 --- a/packages/next-intl/src/navigation/createSharedPathnamesNavigation.tsx +++ b/packages/next-intl/src/navigation/createSharedPathnamesNavigation.tsx @@ -1,17 +1,17 @@ -import usePathname from '../client/usePathname'; -import useRouter from '../client/useRouter'; -import Link from '../link'; -import redirect from '../server/react-client/redirect'; import {AllLocales} from '../shared/types'; +import BaseLink from './BaseLink'; +import baseRedirect from './baseRedirect'; +import useBasePathname from './useBasePathname'; +import useBaseRouter from './useBaseRouter'; export default function createSharedPathnamesNavigation< Locales extends AllLocales // eslint-disable-next-line @typescript-eslint/no-unused-vars -- The value is not used yet, only the type information is important >(opts: {locales: Locales}) { return { - Link: Link as typeof Link, - redirect, - usePathname, - useRouter + Link: BaseLink as typeof BaseLink, + redirect: baseRedirect, + usePathname: useBasePathname, + useRouter: useBaseRouter }; } diff --git a/packages/next-intl/src/navigation/index.tsx b/packages/next-intl/src/navigation/index.tsx index 8b8c71c7f..302f92421 100644 --- a/packages/next-intl/src/navigation/index.tsx +++ b/packages/next-intl/src/navigation/index.tsx @@ -1,5 +1,3 @@ +export {default as createSharedPathnamesNavigation} from './createSharedPathnamesNavigation'; export {default as createLocalizedPathnamesNavigation} from './createLocalizedPathnamesNavigation'; export type {Pathnames} from '../shared/types'; - -// TODO: Possibly release after RFC -// export {default as createSharedPathnamesNavigation} from './createSharedPathnamesNavigation'; diff --git a/packages/next-intl/src/link/react-server/Link.tsx b/packages/next-intl/src/navigation/react-server/BaseLink.tsx similarity index 56% rename from packages/next-intl/src/link/react-server/Link.tsx rename to packages/next-intl/src/navigation/react-server/BaseLink.tsx index c0dcf4648..bcec3a8ea 100644 --- a/packages/next-intl/src/link/react-server/Link.tsx +++ b/packages/next-intl/src/navigation/react-server/BaseLink.tsx @@ -1,19 +1,19 @@ import React, {ComponentProps} from 'react'; import useLocale from '../../react-server/useLocale'; -import BaseLink from '../../shared/BaseLink'; +import BaseLinkWithLocale from '../../shared/BaseLinkWithLocale'; import {AllLocales} from '../../shared/types'; type Props = Omit< - ComponentProps, + ComponentProps, 'locale' > & { locale?: Locales[number]; }; -export default function Link({ +export default function BaseLink({ locale, ...rest }: Props) { const defaultLocale = useLocale(); - return ; + return ; } diff --git a/packages/next-intl/src/navigation/react-server/baseRedirect.tsx b/packages/next-intl/src/navigation/react-server/baseRedirect.tsx new file mode 100644 index 000000000..0a1705b64 --- /dev/null +++ b/packages/next-intl/src/navigation/react-server/baseRedirect.tsx @@ -0,0 +1,11 @@ +import {getRequestLocale} from '../../server/RequestLocale'; +import redirectWithLocale from '../../shared/redirectWithLocale'; +import {ParametersExceptFirstTwo} from '../../shared/types'; + +export default function baseRedirect( + pathname: string, + ...args: ParametersExceptFirstTwo +) { + const locale = getRequestLocale(); + return redirectWithLocale(pathname, locale, ...args); +} diff --git a/packages/next-intl/src/navigation/react-server/createLocalizedPathnamesNavigation.tsx b/packages/next-intl/src/navigation/react-server/createLocalizedPathnamesNavigation.tsx index 69c17690c..4725e5fef 100644 --- a/packages/next-intl/src/navigation/react-server/createLocalizedPathnamesNavigation.tsx +++ b/packages/next-intl/src/navigation/react-server/createLocalizedPathnamesNavigation.tsx @@ -1,7 +1,5 @@ import React, {ComponentProps} from 'react'; -import BaseLink from '../../link/react-server'; import {getRequestLocale} from '../../server/RequestLocale'; -import {redirect as baseRedirect} from '../../server.react-server'; import {AllLocales, ParametersExceptFirst, Pathnames} from '../../shared/types'; import { HrefOrHrefWithParams, @@ -9,6 +7,8 @@ import { compileLocalizedPathname, normalizeNameOrNameWithParams } from '../utils'; +import BaseLink from './BaseLink'; +import baseRedirect from './baseRedirect'; export default function createLocalizedPathnamesNavigation< Locales extends AllLocales, diff --git a/packages/next-intl/src/navigation/react-server/createSharedPathnamesNavigation.tsx b/packages/next-intl/src/navigation/react-server/createSharedPathnamesNavigation.tsx index 8f6bd9e77..58458c2cd 100644 --- a/packages/next-intl/src/navigation/react-server/createSharedPathnamesNavigation.tsx +++ b/packages/next-intl/src/navigation/react-server/createSharedPathnamesNavigation.tsx @@ -1,6 +1,6 @@ -import Link from '../../link/react-server'; -import redirect from '../../server/redirect'; import {AllLocales} from '../../shared/types'; +import BaseLink from './BaseLink'; +import baseRedirect from './baseRedirect'; export default function createSharedPathnamesNavigation< Locales extends AllLocales @@ -15,8 +15,8 @@ export default function createSharedPathnamesNavigation< } return { - Link: Link, - redirect, + Link: BaseLink, + redirect: baseRedirect, usePathname: notSupported('usePathname'), useRouter: notSupported('useRouter') }; diff --git a/packages/next-intl/src/navigation/react-server/index.tsx b/packages/next-intl/src/navigation/react-server/index.tsx index 6b30ca63b..20649060b 100644 --- a/packages/next-intl/src/navigation/react-server/index.tsx +++ b/packages/next-intl/src/navigation/react-server/index.tsx @@ -1 +1,2 @@ +export {default as createSharedPathnamesNavigation} from './createSharedPathnamesNavigation'; export {default as createLocalizedPathnamesNavigation} from './createLocalizedPathnamesNavigation'; diff --git a/packages/next-intl/src/client/usePathname.tsx b/packages/next-intl/src/navigation/useBasePathname.tsx similarity index 95% rename from packages/next-intl/src/client/usePathname.tsx rename to packages/next-intl/src/navigation/useBasePathname.tsx index 83ab390c1..e4625a09b 100644 --- a/packages/next-intl/src/client/usePathname.tsx +++ b/packages/next-intl/src/navigation/useBasePathname.tsx @@ -18,7 +18,7 @@ import {hasPathnamePrefixed, unlocalizePathname} from '../shared/utils'; * const pathname = usePathname(); * ``` */ -export default function usePathname(): string { +export default function useBasePathname(): string { // The types aren't entirely correct here. Outside of Next.js // `useParams` can be called, but the return type is `null`. const pathname = useNextPathname() as ReturnType< diff --git a/packages/next-intl/src/client/useRouter.tsx b/packages/next-intl/src/navigation/useBaseRouter.tsx similarity index 97% rename from packages/next-intl/src/client/useRouter.tsx rename to packages/next-intl/src/navigation/useBaseRouter.tsx index 206836f98..1c5569e62 100644 --- a/packages/next-intl/src/client/useRouter.tsx +++ b/packages/next-intl/src/navigation/useBaseRouter.tsx @@ -27,7 +27,7 @@ type IntlNavigateOptions = { * router.push('/about', {locale: 'de'}); * ``` */ -export default function useRouter() { +export default function useBaseRouter() { const router = useNextRouter(); const locale = useLocale(); diff --git a/packages/next-intl/src/react-client/Link.tsx b/packages/next-intl/src/react-client/Link.tsx deleted file mode 100644 index db45dd493..000000000 --- a/packages/next-intl/src/react-client/Link.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React, {ComponentProps} from 'react'; -import Link from '../link'; - -// TODO: Only available for backwards compatibility -// during the beta, remove for stable release - -let hasWarned = false; - -/** @deprecated Is available as `import Link from 'next-intl/link'` now. */ -export default function LinkDeprecated(props: ComponentProps) { - if (process.env.NODE_ENV !== 'production' && !hasWarned) { - console.warn( - `\n\nDEPRECATION WARNING: The import for \`Link\` from next-intl has changed. - -Previously: import {Link} from 'next-intl'; -Now: import Link from 'next-intl/link'; - -Please upgrade your import accordingly. See also https://next-intl-docs.vercel.app/docs/routing/navigation#link\n\n` - ); - hasWarned = true; - } - - return ; -} diff --git a/packages/next-intl/src/react-client/index.tsx b/packages/next-intl/src/react-client/index.tsx index 8c528b861..ff5365987 100644 --- a/packages/next-intl/src/react-client/index.tsx +++ b/packages/next-intl/src/react-client/index.tsx @@ -12,7 +12,6 @@ import { useTranslations as base_useTranslations, useFormatter as base_useFormatter } from 'use-intl'; -import Link from './Link'; export * from 'use-intl'; @@ -44,13 +43,3 @@ export const useFormatter = callHook( export {default as useLocale} from './useLocale'; export {default as NextIntlClientProvider} from '../shared/NextIntlClientProvider'; - -// Legacy export (TBD if we'll deprecate this in favour of `NextIntlClientProvider`) -export {default as NextIntlProvider} from '../shared/NextIntlClientProvider'; - -/** @deprecated Is available as `import Link from 'next-intl/link'` now. */ -export const LocalizedLink = Link; // TODO: Remove, this is only for compatibility in the RSC beta and would break Next.js 12 -/** @deprecated Is available as `import Link from 'next-intl/link'` now. */ -export {default as Link} from './Link'; // TODO: Remove, this is only for compatibility in the RSC beta and would break Next.js 12 - -export {default as useLocalizedRouter} from './useLocalizedRouter'; diff --git a/packages/next-intl/src/react-client/useLocalizedRouter.tsx b/packages/next-intl/src/react-client/useLocalizedRouter.tsx deleted file mode 100644 index 24f8c8e0e..000000000 --- a/packages/next-intl/src/react-client/useLocalizedRouter.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import useRouter from '../client/useRouter'; - -// TODO: Only available for backwards compatibility -// during the beta, remove for stable release - -let hasWarned = false; - -export default function useLocalizedRouterDeprecated() { - if (process.env.NODE_ENV !== 'production' && !hasWarned) { - console.warn( - `\n\nDEPRECATION WARNING: The \`useLocalizedRouter\` import from \`next-intl\` is deprecated and will be removed in the stable release of next-intl. Please import \`useLocalizedRouter\` from \`next-intl/client\` instead. See https://next-intl-docs.vercel.app/docs/getting-started/app-router-server-components\n\n` - ); - hasWarned = true; - } - - return useRouter(); -} diff --git a/packages/next-intl/src/react-server/Link.tsx b/packages/next-intl/src/react-server/Link.tsx deleted file mode 100644 index ea06938a8..000000000 --- a/packages/next-intl/src/react-server/Link.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React, {ComponentProps} from 'react'; -import Link from '../link/react-server'; - -// TODO: Only available for backwards compatibility -// during the beta, remove for stable release - -let hasWarned = false; - -/** @deprecated Is available as `import Link from 'next-intl/link'` now. */ -export default function LinkDeprecated(props: ComponentProps) { - if (process.env.NODE_ENV !== 'production' && !hasWarned) { - console.warn( - `\n\nDEPRECATION WARNING: The import for \`Link\` from next-intl has changed. - -Previously: import {Link} from 'next-intl'; -Now: import Link from 'next-intl/link'; - -Please upgrade your import accordingly. See also https://next-intl-docs.vercel.app/docs/routing/navigation#link\n\n` - ); - hasWarned = true; - } - - return ; -} diff --git a/packages/next-intl/src/react-server/index.tsx b/packages/next-intl/src/react-server/index.tsx index fbabe2f1c..59ad379b8 100644 --- a/packages/next-intl/src/react-server/index.tsx +++ b/packages/next-intl/src/react-server/index.tsx @@ -6,12 +6,9 @@ * Make sure this mirrors the API from '../react-client'. */ -import Link from './Link'; - // Replaced exports from the `react` package export {default as useLocale} from './useLocale'; export {default as useTranslations} from './useTranslations'; -export {default as useIntl} from './useIntl'; export {default as useFormatter} from './useFormatter'; export {default as useNow} from './useNow'; export {default as useTimeZone} from './useTimeZone'; @@ -20,9 +17,3 @@ export {default as NextIntlClientProvider} from './NextIntlClientProvider'; // Everything from `core` export * from 'use-intl/core'; - -/** @deprecated Is called `Link` now. */ -export const LocalizedLink = Link; - -// Deprecation handled within component -export {default as Link} from './Link'; diff --git a/packages/next-intl/src/react-server/useIntl.tsx b/packages/next-intl/src/react-server/useIntl.tsx deleted file mode 100644 index 8c0853fe9..000000000 --- a/packages/next-intl/src/react-server/useIntl.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import getIntl from '../server/getIntl'; -import useHook from './useHook'; - -let hasWarned = false; - -export default function useIntl() { - if (process.env.NODE_ENV !== 'production' && !hasWarned) { - hasWarned = true; - console.warn( - '`useIntl()` is deprecated and will be removed in the next major version. Please switch to `useFormatter()`.' - ); - } - - return useHook('useIntl', getIntl()); -} diff --git a/packages/next-intl/src/server/getConfig.tsx b/packages/next-intl/src/server/getConfig.tsx index c2635a669..4fb5e2548 100644 --- a/packages/next-intl/src/server/getConfig.tsx +++ b/packages/next-intl/src/server/getConfig.tsx @@ -5,6 +5,12 @@ import createRequestConfig from '../server/createRequestConfig'; // Make sure `now` is consistent across the request in case none was configured const getDefaultNow = cache(() => new Date()); +// This is automatically inherited by `NextIntlClientProvider` if +// the component is rendered from a Server Component +const getDefaultTimeZone = cache( + () => Intl.DateTimeFormat().resolvedOptions().timeZone +); + const receiveRuntimeConfig = cache( async (locale: string, getConfig?: typeof createRequestConfig) => { let result = getConfig?.({locale}); @@ -13,7 +19,8 @@ const receiveRuntimeConfig = cache( } return { ...result, - now: result?.now || getDefaultNow() + now: result?.now || getDefaultNow(), + timeZone: result?.timeZone || getDefaultTimeZone() }; } ); diff --git a/packages/next-intl/src/server/getIntl.tsx b/packages/next-intl/src/server/getIntl.tsx deleted file mode 100644 index d57e4ddb9..000000000 --- a/packages/next-intl/src/server/getIntl.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import {cache} from 'react'; -import {createIntl} from 'use-intl/core'; -import {getRequestLocale} from './RequestLocale'; -import getConfig from './getConfig'; - -let hasWarned = false; - -/** @deprecated Please switch to `getFormatter`. */ -const getIntl = cache(async () => { - if (process.env.NODE_ENV !== 'production' && !hasWarned) { - hasWarned = true; - console.warn( - ` -\`getIntl()\` is deprecated and will be removed in the next major version. Please switch to \`getFormatter()\`. - -Learn more: https://next-intl-docs.vercel.app/docs/environments/metadata-route-handlers -` - ); - } - - const locale = getRequestLocale(); - const config = await getConfig(locale); - return createIntl(config); -}); - -export default getIntl; diff --git a/packages/next-intl/src/server/getTranslations.tsx b/packages/next-intl/src/server/getTranslations.tsx deleted file mode 100644 index daa46ae2e..000000000 --- a/packages/next-intl/src/server/getTranslations.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import {cache} from 'react'; -import { - createTranslator, - Formats, - TranslationValues, - RichTranslationValuesPlain, - MessageKeys, - NamespaceKeys, - NestedKeyOf, - NestedValueOf -} from 'use-intl/core'; -import {getRequestLocale} from './RequestLocale'; -import getConfig from './getConfig'; - -let hasWarned = false; - -async function getTranslationsImpl< - NestedKey extends NamespaceKeys< - IntlMessages, - NestedKeyOf - > = never ->( - namespace?: NestedKey -): // Explicitly defining the return type is necessary as TypeScript would get it wrong -Promise<{ - // Default invocation - < - TargetKey extends MessageKeys< - NestedValueOf< - {'!': IntlMessages}, - [NestedKey] extends [never] ? '!' : `!.${NestedKey}` - >, - NestedKeyOf< - NestedValueOf< - {'!': IntlMessages}, - [NestedKey] extends [never] ? '!' : `!.${NestedKey}` - > - > - > - >( - key: TargetKey, - values?: TranslationValues, - formats?: Partial - ): string; - - // `rich` - rich< - TargetKey extends MessageKeys< - NestedValueOf< - {'!': IntlMessages}, - [NestedKey] extends [never] ? '!' : `!.${NestedKey}` - >, - NestedKeyOf< - NestedValueOf< - {'!': IntlMessages}, - [NestedKey] extends [never] ? '!' : `!.${NestedKey}` - > - > - > - >( - key: TargetKey, - values?: RichTranslationValuesPlain, - formats?: Partial - ): string; - - // `raw` - raw< - TargetKey extends MessageKeys< - NestedValueOf< - {'!': IntlMessages}, - [NestedKey] extends [never] ? '!' : `!.${NestedKey}` - >, - NestedKeyOf< - NestedValueOf< - {'!': IntlMessages}, - [NestedKey] extends [never] ? '!' : `!.${NestedKey}` - > - > - > - >( - key: TargetKey - ): any; -}> { - if (process.env.NODE_ENV !== 'production' && !hasWarned) { - console.warn(` -\`getTranslations\` is deprecated, please switch to \`getTranslator\`. - -Learn more: https://next-intl-docs.vercel.app/docs/environments/metadata-route-handlers - `); - hasWarned = true; - } - - const locale = getRequestLocale(); - const config = await getConfig(locale); - - return createTranslator({ - ...config, - namespace, - messages: config.messages || {} - }); -} - -/** @deprecated Is called `getTranslator` now. */ -export default cache(getTranslationsImpl); diff --git a/packages/next-intl/src/server/index.tsx b/packages/next-intl/src/server/index.tsx index 8d38b5ea6..bd913e4ba 100644 --- a/packages/next-intl/src/server/index.tsx +++ b/packages/next-intl/src/server/index.tsx @@ -2,50 +2,13 @@ * Server-only APIs available via `next-intl/server`. */ -import createMiddleware_ from '../middleware'; -import MiddlewareConfig from '../middleware/NextIntlMiddlewareConfig'; -import {AllLocales} from '../shared/types'; - -let hasWarnedForMiddlewareImport = false; -/** @deprecated Should be imported as `import createMiddleware from 'next-intl/middleware', not from `next-intl/server`. */ -export function createIntlMiddleware( - config: MiddlewareConfig -) { - if (process.env.NODE_ENV !== 'production' && !hasWarnedForMiddlewareImport) { - hasWarnedForMiddlewareImport = true; - console.warn( - ` -Importing \`createMiddleware\` from \`next-intl/server\` is deprecated. Please update the import and add a \`matcher\`: - - // middleware.ts - import createMiddleware from 'next-intl/middleware'; - - // ... - - export const config = { - // Skip all paths that should not be internationalized - matcher: ['/((?!api|_next|.*\\\\..*).*)'] - }; -` - ); - } - return createMiddleware_({ - ...config, - // @ts-expect-error - _matcher: ['/((?!api|_next|.*\\..*).*)'] - }); -} - // Must match `./react-client/index.tsx` export {default as getRequestConfig} from './getRequestConfig'; -export {default as getIntl} from './getIntl'; export {default as getFormatter} from './getFormatter'; export {default as getLocale} from './getLocale'; export {default as getNow} from './getNow'; export {default as getTimeZone} from './getTimeZone'; -export {default as getTranslations} from './getTranslations'; export {default as getTranslator} from './getTranslator'; export {default as getMessages} from './getMessages'; -export {default as redirect} from './redirect'; export {setRequestLocale as unstable_setRequestLocale} from './RequestLocale'; diff --git a/packages/next-intl/src/server/react-client/index.tsx b/packages/next-intl/src/server/react-client/index.tsx index edef0cd4f..f39fdf4cc 100644 --- a/packages/next-intl/src/server/react-client/index.tsx +++ b/packages/next-intl/src/server/react-client/index.tsx @@ -1,11 +1,8 @@ import type { getRequestConfig as getRequestConfig_type, - getIntl as getIntl_type, getFormatter as getFormatter_type, - getLocale as getLocale_type, getNow as getNow_type, getTimeZone as getTimeZone_type, - getTranslations as getTranslations_type, getTranslator as getTranslator_type, getMessages as getMessages_type, unstable_setRequestLocale as unstable_setRequestLocale_type @@ -26,28 +23,14 @@ function notSupported(name: string) { // prettier-ignore export const getRequestConfig = (() => notSupported('getRequestConfig')) as unknown as typeof getRequestConfig_type; // prettier-ignore -/** @deprecated Is called `getFormatter` now. */ -export const getIntl = notSupported('getIntl') as unknown as typeof getIntl_type; -// prettier-ignore export const getFormatter = notSupported('getFormatter') as unknown as typeof getFormatter_type; // prettier-ignore -/** @deprecated Please use the `locale` parameter from Next.js instead. */ -export const getLocale = notSupported('getLocale') as unknown as typeof getLocale_type; -// prettier-ignore export const getNow = notSupported('getNow') as unknown as typeof getNow_type; // prettier-ignore export const getTimeZone = notSupported('getTimeZone') as unknown as typeof getTimeZone_type; // prettier-ignore -/** @deprecated Is called `getTranslator` now. */ -export const getTranslations = notSupported('getTranslations') as unknown as typeof getTranslations_type; -// prettier-ignore export const getTranslator = notSupported('getTranslator') as unknown as typeof getTranslator_type; // prettier-ignore export const getMessages = notSupported('getMessages') as unknown as typeof getMessages_type; // prettier-ignore export const unstable_setRequestLocale = notSupported('unstable_setRequestLocale') as unknown as typeof unstable_setRequestLocale_type; - -// TODO: Since this is available in Client Comonents too, we should really -// consider exporting this from `next-intl/navigation` instead. For now, for -// compatibility, we'll add a client entry point for `next-intl/server`. -export {default as redirect} from './redirect'; diff --git a/packages/next-intl/src/server/redirect.tsx b/packages/next-intl/src/server/redirect.tsx deleted file mode 100644 index ca70e4f37..000000000 --- a/packages/next-intl/src/server/redirect.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import {ParametersExceptFirstTwo} from '../shared/types'; -import {getRequestLocale} from './RequestLocale'; -import baseRedirect from './baseRedirect'; - -export default function redirect( - pathname: string, - ...args: ParametersExceptFirstTwo -) { - const locale = getRequestLocale(); - return baseRedirect(pathname, locale, ...args); -} diff --git a/packages/next-intl/src/shared/BaseLink.tsx b/packages/next-intl/src/shared/BaseLinkWithLocale.tsx similarity index 93% rename from packages/next-intl/src/shared/BaseLink.tsx rename to packages/next-intl/src/shared/BaseLinkWithLocale.tsx index fe41c88b3..df5d01b41 100644 --- a/packages/next-intl/src/shared/BaseLink.tsx +++ b/packages/next-intl/src/shared/BaseLinkWithLocale.tsx @@ -10,7 +10,10 @@ type Props = Omit, 'locale'> & { locale: string; }; -function BaseLink({href, locale, prefetch, ...rest}: Props, ref: Props['ref']) { +function BaseLinkWithLocale( + {href, locale, prefetch, ...rest}: Props, + ref: Props['ref'] +) { // The types aren't entirely correct here. Outside of Next.js // `useParams` can be called, but the return type is `null`. const pathname = usePathname() as ReturnType | null; @@ -54,4 +57,4 @@ function BaseLink({href, locale, prefetch, ...rest}: Props, ref: Props['ref']) { ); } -export default forwardRef(BaseLink); +export default forwardRef(BaseLinkWithLocale); diff --git a/packages/next-intl/src/shared/NextIntlClientProvider.tsx b/packages/next-intl/src/shared/NextIntlClientProvider.tsx index 6cf5eb41d..2b0385ae4 100644 --- a/packages/next-intl/src/shared/NextIntlClientProvider.tsx +++ b/packages/next-intl/src/shared/NextIntlClientProvider.tsx @@ -9,23 +9,10 @@ type Props = Omit, 'locale'> & { locale?: string; }; -export default function NextIntlClientProvider({ - children, - locale, - now, - ...rest -}: Props) { +export default function NextIntlClientProvider({locale, ...rest}: Props) { // TODO: We could call `useParams` here to receive a default value // for `locale`, but this would require dropping Next.js <13. - // TODO: This is no longer necessary, remove for stable release - if (process.env.NODE_ENV !== 'production' && typeof now === 'string') { - console.error( - 'Passing an ISO date string to `NextIntlClientProvider` is deprecated since React Server Components have built-in support for serializing dates now. To upgrade, pass a `Date` instance instead.' - ); - now = new Date(now); - } - if (!locale) { throw new Error( process.env.NODE_ENV !== 'production' @@ -34,9 +21,5 @@ export default function NextIntlClientProvider({ ); } - return ( - - {children} - - ); + return ; } diff --git a/packages/next-intl/src/server/baseRedirect.tsx b/packages/next-intl/src/shared/redirectWithLocale.tsx similarity index 64% rename from packages/next-intl/src/server/baseRedirect.tsx rename to packages/next-intl/src/shared/redirectWithLocale.tsx index 177da8429..f14eb9e81 100644 --- a/packages/next-intl/src/server/baseRedirect.tsx +++ b/packages/next-intl/src/shared/redirectWithLocale.tsx @@ -1,8 +1,8 @@ import {redirect as nextRedirect} from 'next/navigation'; -import {AllLocales, ParametersExceptFirst} from '../shared/types'; -import {localizePathname} from '../shared/utils'; +import {AllLocales, ParametersExceptFirst} from './types'; +import {localizePathname} from './utils'; -export default function baseRedirect( +export default function redirectWithLocale( pathname: string, locale: AllLocales[number], ...args: ParametersExceptFirst diff --git a/packages/next-intl/test/middleware/middleware.test.tsx b/packages/next-intl/test/middleware/middleware.test.tsx index d64ff332a..38df4dfe0 100644 --- a/packages/next-intl/test/middleware/middleware.test.tsx +++ b/packages/next-intl/test/middleware/middleware.test.tsx @@ -119,7 +119,8 @@ describe('prefix-based routing', () => { describe('localePrefix: as-needed', () => { const middleware = createIntlMiddleware({ defaultLocale: 'en', - locales: ['en', 'de'] + locales: ['en', 'de'], + localePrefix: 'as-needed' }); it('rewrites requests for the default locale', () => { @@ -287,6 +288,7 @@ describe('prefix-based routing', () => { const middlewareWithPathnames = createIntlMiddleware({ defaultLocale: 'en', locales: ['en', 'de'], + localePrefix: 'as-needed', pathnames: { '/': '/', '/about': { @@ -515,6 +517,7 @@ describe('prefix-based routing', () => { const callMiddleware = createIntlMiddleware({ defaultLocale: 'en', locales: ['en', 'de'], + localePrefix: 'as-needed', pathnames: { '/a': { en: '/one', @@ -1096,6 +1099,7 @@ describe('domain-based routing', () => { const middleware = createIntlMiddleware({ defaultLocale: 'en', locales: ['en', 'fr'], + localePrefix: 'as-needed', domains: [ {defaultLocale: 'en', domain: 'en.example.com', locales: ['en']}, { @@ -1335,6 +1339,7 @@ describe('domain-based routing', () => { const middlewareWithPathnames = createIntlMiddleware({ defaultLocale: 'en', locales: ['en', 'fr'], + localePrefix: 'as-needed', domains: [ {defaultLocale: 'en', domain: 'en.example.com', locales: ['en']}, { @@ -1754,120 +1759,3 @@ describe('domain-based routing', () => { }); }); }); - -describe('deprecated domain config', () => { - it("accepts deprecated config with `routing.type: 'prefix'`", () => { - const middleware = createIntlMiddleware({ - defaultLocale: 'en', - locales: ['en', 'de'], - routing: { - type: 'prefix', - prefix: 'always' - } - }); - - middleware(createMockRequest('/', 'en', 'http://example.com')); - middleware(createMockRequest('/about', 'en', 'http://example.com')); - middleware(createMockRequest('/de/about', 'de', 'http://example.com')); - - expect(MockedNextResponse.next).not.toHaveBeenCalled(); - expect(MockedNextResponse.redirect).toHaveBeenCalledTimes(2); - expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( - 'http://example.com/en' - ); - expect(MockedNextResponse.redirect.mock.calls[1][0].toString()).toBe( - 'http://example.com/en/about' - ); - expect(MockedNextResponse.rewrite).toHaveBeenCalled(); - expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( - 'http://example.com/de/about' - ); - }); - - it("accepts deprecated config with `routing.type: 'domain'`", () => { - const middleware = createIntlMiddleware({ - defaultLocale: 'en', - locales: ['en', 'de'], - routing: { - type: 'domain', - domains: [ - { - locale: 'en', - domain: 'en.example.com' - }, - { - locale: 'de', - domain: 'de.example.com' - } - ] - } - }); - - middleware(createMockRequest('/', 'en', 'http://en.example.com')); - middleware(createMockRequest('/about', 'en', 'http://en.example.com')); - - expect(MockedNextResponse.next).not.toHaveBeenCalled(); - expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - - expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( - 'http://en.example.com/en' - ); - expect(MockedNextResponse.rewrite.mock.calls[1][0].toString()).toBe( - 'http://en.example.com/en/about' - ); - - middleware(createMockRequest('/en/about', 'en', 'http://en.example.com')); - expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( - 'http://en.example.com/about' - ); - - middleware(createMockRequest('/de/help', 'de', 'http://en.example.com')); - - expect(MockedNextResponse.redirect.mock.calls[1][0].toString()).toBe( - 'http://de.example.com/help' - ); - }); - - it('accepts deprecated config with `domain.locale`', () => { - const middleware = createIntlMiddleware({ - defaultLocale: 'en', - locales: ['en', 'de'], - domains: [ - // @ts-expect-error Deprecated config - { - locale: 'en', - domain: 'en.example.com' - }, - // @ts-expect-error Deprecated config - { - locale: 'de', - domain: 'de.example.com' - } - ] - }); - - middleware(createMockRequest('/', 'en', 'http://en.example.com')); - middleware(createMockRequest('/about', 'en', 'http://en.example.com')); - - expect(MockedNextResponse.next).not.toHaveBeenCalled(); - expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - - expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( - 'http://en.example.com/en' - ); - expect(MockedNextResponse.rewrite.mock.calls[1][0].toString()).toBe( - 'http://en.example.com/en/about' - ); - - middleware(createMockRequest('/en/about', 'en', 'http://en.example.com')); - expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( - 'http://en.example.com/about' - ); - - middleware(createMockRequest('/de/help', 'de', 'http://en.example.com')); - - expect(MockedNextResponse.redirect.mock.calls[1][0].toString()).toBe( - 'http://de.example.com/help' - ); - }); -}); diff --git a/packages/next-intl/test/link/Link.test.tsx b/packages/next-intl/test/navigation/BaseLink.test.tsx similarity index 81% rename from packages/next-intl/test/link/Link.test.tsx rename to packages/next-intl/test/navigation/BaseLink.test.tsx index af0ba14e9..5f0f887bc 100644 --- a/packages/next-intl/test/link/Link.test.tsx +++ b/packages/next-intl/test/navigation/BaseLink.test.tsx @@ -3,7 +3,7 @@ import {usePathname, useParams} from 'next/navigation'; import React from 'react'; import {it, describe, vi, beforeEach, expect} from 'vitest'; import {NextIntlClientProvider} from '../../src'; -import Link from '../../src/link'; +import BaseLink from '../../src/navigation/BaseLink'; vi.mock('next/navigation'); @@ -14,14 +14,16 @@ describe('unprefixed routing', () => { }); it('renders an href without a locale if the locale matches', () => { - render(Test); + render(Test); expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe( '/test' ); }); it('renders an href without a locale if the locale matches for an object href', () => { - render(Test); + render( + Test + ); expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe( '/test?foo=bar' ); @@ -29,9 +31,9 @@ describe('unprefixed routing', () => { it('renders an href with a locale if the locale changes', () => { render( - + Test - + ); expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe( '/de/test' @@ -40,9 +42,9 @@ describe('unprefixed routing', () => { it('renders an href with a locale if the locale changes for an object href', () => { render( - + Test - + ); expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe( '/de/test' @@ -50,7 +52,7 @@ describe('unprefixed routing', () => { }); it('works for external urls', () => { - render(Test); + render(Test); expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe( 'https://example.com' ); @@ -58,7 +60,7 @@ describe('unprefixed routing', () => { it('works for external urls with an object href', () => { render( - { }} > Test - + ); expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe( 'https://example.com/test' @@ -77,14 +79,14 @@ describe('unprefixed routing', () => { let ref; render( - { ref = node; }} href="/test" > Test - + ); expect(ref).toBeDefined(); @@ -98,14 +100,14 @@ describe('prefixed routing', () => { }); it('renders an href with a locale if the locale matches', () => { - render(Test); + render(Test); expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe( '/en/test' ); }); it('renders an href without a locale if the locale matches for an object href', () => { - render(Test); + render(Test); expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe( '/en/test' ); @@ -113,9 +115,9 @@ describe('prefixed routing', () => { it('renders an href with a locale if the locale changes', () => { render( - + Test - + ); expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe( '/de/test' @@ -124,9 +126,9 @@ describe('prefixed routing', () => { it('renders an href with a locale if the locale changes for an object href', () => { render( - + Test - + ); expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe( '/de/test' @@ -134,7 +136,7 @@ describe('prefixed routing', () => { }); it('works for external urls', () => { - render(Test); + render(Test); expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe( 'https://example.com' ); @@ -142,7 +144,7 @@ describe('prefixed routing', () => { it('works for external urls with an object href', () => { render( - { }} > Test - + ); expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe( 'https://example.com/test' @@ -166,7 +168,7 @@ describe('usage outside of Next.js', () => { it('works with a provider', () => { render( - Test + Test ); expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe( @@ -175,7 +177,7 @@ describe('usage outside of Next.js', () => { }); it('throws without a provider', () => { - expect(() => render(Test)).toThrow( + expect(() => render(Test)).toThrow( 'No intl context found. Have you configured the provider?' ); }); diff --git a/packages/next-intl/test/client/usePathname.test.tsx b/packages/next-intl/test/navigation/useBasePathname.test.tsx similarity index 94% rename from packages/next-intl/test/client/usePathname.test.tsx rename to packages/next-intl/test/navigation/useBasePathname.test.tsx index b1a042b19..44137584d 100644 --- a/packages/next-intl/test/client/usePathname.test.tsx +++ b/packages/next-intl/test/navigation/useBasePathname.test.tsx @@ -3,7 +3,7 @@ import {usePathname as useNextPathname, useParams} from 'next/navigation'; import React from 'react'; import {it, describe, vi, beforeEach, expect} from 'vitest'; import {NextIntlClientProvider} from '../../src'; -import {usePathname} from '../../src/client'; +import useBasePathname from '../../src/navigation/useBasePathname'; vi.mock('next/navigation'); @@ -13,7 +13,7 @@ function mockPathname(pathname: string) { } function Component() { - return <>{usePathname()}; + return <>{useBasePathname()}; } describe('unprefixed routing', () => { diff --git a/packages/next-intl/test/client/useRouter.test.tsx b/packages/next-intl/test/navigation/useBaseRouter.test.tsx similarity index 95% rename from packages/next-intl/test/client/useRouter.test.tsx rename to packages/next-intl/test/navigation/useBaseRouter.test.tsx index 1a3706408..b32a534b5 100644 --- a/packages/next-intl/test/client/useRouter.test.tsx +++ b/packages/next-intl/test/navigation/useBaseRouter.test.tsx @@ -4,7 +4,7 @@ import {AppRouterInstance} from 'next/dist/shared/lib/app-router-context.shared- import {useRouter as useNextRouter} from 'next/navigation'; import React, {useEffect} from 'react'; import {it, describe, vi, beforeEach, expect} from 'vitest'; -import {useRouter} from '../../src/client'; +import useBaseRouter from '../../src/navigation/useBaseRouter'; vi.mock('next/navigation', () => { const router: AppRouterInstance = { @@ -21,9 +21,9 @@ vi.mock('next/navigation', () => { }; }); -function callRouter(cb: (router: ReturnType) => void) { +function callRouter(cb: (router: ReturnType) => void) { function Component() { - const router = useRouter(); + const router = useBaseRouter(); useEffect(() => { cb(router); }, [router]); diff --git a/packages/next-intl/withNextIntl.d.ts b/packages/next-intl/withNextIntl.d.ts deleted file mode 100644 index 97e0c8bf3..000000000 --- a/packages/next-intl/withNextIntl.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {NextConfig} from 'next'; - -type NextConfigWithI18n = NextConfig & {i18nConfig?: string}; - -export default function withNextIntl(config: NextConfigWithI18n): NextConfig; diff --git a/packages/next-intl/withNextIntl.js b/packages/next-intl/withNextIntl.js deleted file mode 100644 index 0429151dd..000000000 --- a/packages/next-intl/withNextIntl.js +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-env node */ - -// TODO: Only available for backwards compatibility -// during the beta, remove for stable release - -const {initPlugin} = require('./plugin'); - -module.exports = function withNextIntl(enhancedNextConfig) { - console.warn( - `\n\nDEPRECATION WARNING: The \`withNextIntl()\` function is deprecated and will be removed in the stable release of next-intl. Please use \`next-intl/plugin\` instead. See https://next-intl-docs.vercel.app/docs/next-13/server-components\n\n` - ); - return initPlugin(enhancedNextConfig.i18nConfig, enhancedNextConfig); -}; diff --git a/packages/use-intl/package.json b/packages/use-intl/package.json index 2366881f5..1d4c9300f 100644 --- a/packages/use-intl/package.json +++ b/packages/use-intl/package.json @@ -85,7 +85,7 @@ "size-limit": [ { "path": "dist/production/index.js", - "limit": "12.29 kB" + "limit": "12.3 kB" } ] } diff --git a/packages/use-intl/src/core/IntlConfig.tsx b/packages/use-intl/src/core/IntlConfig.tsx index 2d46c8c69..e7109b5a3 100644 --- a/packages/use-intl/src/core/IntlConfig.tsx +++ b/packages/use-intl/src/core/IntlConfig.tsx @@ -31,7 +31,7 @@ type IntlConfig = { /** * Providing this value will have two effects: * 1. It will be used as the default for the `now` argument of - * `useIntl().formatRelativeTime` if no explicit value is provided. + * `useFormatter().formatRelativeTime` if no explicit value is provided. * 2. It will be returned as a static value from the `useNow` hook. Note * however that when `updateInterval` is configured on the `useNow` hook, * the global `now` value will only be used for the initial render, but diff --git a/packages/use-intl/src/core/createIntl.tsx b/packages/use-intl/src/core/createIntl.tsx deleted file mode 100644 index 9c34b36a9..000000000 --- a/packages/use-intl/src/core/createIntl.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import createFormatter from './createFormatter'; - -/** @deprecated Switch to `createFormatter` */ -export default function createIntl( - ...args: Parameters -) { - const formatter = createFormatter(...args); - return { - formatDateTime: formatter.dateTime, - formatNumber: formatter.number, - formatRelativeTime: formatter.relativeTime - }; -} diff --git a/packages/use-intl/src/core/index.tsx b/packages/use-intl/src/core/index.tsx index 89c7a380e..2271be6e0 100644 --- a/packages/use-intl/src/core/index.tsx +++ b/packages/use-intl/src/core/index.tsx @@ -17,6 +17,3 @@ export type {default as MessageKeys} from './utils/MessageKeys'; export type {default as NamespaceKeys} from './utils/NamespaceKeys'; export type {default as NestedKeyOf} from './utils/NestedKeyOf'; export type {default as NestedValueOf} from './utils/NestedValueOf'; - -// TODO: Remove in next major version -export {default as createIntl} from './createIntl'; diff --git a/packages/use-intl/src/react/getInitializedConfig.tsx b/packages/use-intl/src/react/getInitializedConfig.tsx deleted file mode 100644 index 87230af1f..000000000 --- a/packages/use-intl/src/react/getInitializedConfig.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// TODO: Only for backwards compatibility, remove in next major version -export {default} from '../core/initializeConfig'; diff --git a/packages/use-intl/src/react/index.tsx b/packages/use-intl/src/react/index.tsx index 88a8f3959..1747d2829 100644 --- a/packages/use-intl/src/react/index.tsx +++ b/packages/use-intl/src/react/index.tsx @@ -5,6 +5,3 @@ export {default as useNow} from './useNow'; export {default as useTimeZone} from './useTimeZone'; export {default as useMessages} from './useMessages'; export {default as useFormatter} from './useFormatter'; - -// TODO: Remove in next major version -export {default as useIntl} from './useIntl'; diff --git a/packages/use-intl/src/react/useIntl.tsx b/packages/use-intl/src/react/useIntl.tsx deleted file mode 100644 index 57792d2bc..000000000 --- a/packages/use-intl/src/react/useIntl.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import {useMemo} from 'react'; -import createIntl from '../core/createIntl'; -import useIntlContext from './useIntlContext'; - -let hasWarned = false; - -/** @deprecated Switch to `useFormatter` instead. */ -export default function useIntl() { - const {formats, locale, now: globalNow, onError, timeZone} = useIntlContext(); - - if (process.env.NODE_ENV !== 'production' && !hasWarned) { - hasWarned = true; - console.warn( - '`useIntl()` is deprecated and will be removed in the next major version. Please switch to `useFormatter()`.' - ); - } - - return useMemo( - () => - createIntl({ - formats, - locale, - now: globalNow, - onError, - timeZone - }), - [formats, globalNow, locale, onError, timeZone] - ); -} diff --git a/packages/use-intl/src/react/useTranslationsImpl.tsx b/packages/use-intl/src/react/useTranslationsImpl.tsx index 9bb5a835f..80dc55207 100644 --- a/packages/use-intl/src/react/useTranslationsImpl.tsx +++ b/packages/use-intl/src/react/useTranslationsImpl.tsx @@ -1,10 +1,14 @@ import {useMemo} from 'react'; +import {IntlError, IntlErrorCode} from '../core'; import AbstractIntlMessages from '../core/AbstractIntlMessages'; import createBaseTranslator from '../core/createBaseTranslator'; import resolveNamespace from '../core/resolveNamespace'; import NestedKeyOf from '../core/utils/NestedKeyOf'; import useIntlContext from './useIntlContext'; +let hasWarnedForMissingTimezone = false; +const isServer = typeof window === 'undefined'; + export default function useTranslationsImpl< Messages extends AbstractIntlMessages, NestedKey extends NestedKeyOf @@ -24,6 +28,18 @@ export default function useTranslationsImpl< allMessages = allMessages[namespacePrefix] as Messages; namespace = resolveNamespace(namespace, namespacePrefix) as NestedKey; + if (!timeZone && !hasWarnedForMissingTimezone && isServer) { + hasWarnedForMissingTimezone = true; + onError( + new IntlError( + IntlErrorCode.ENVIRONMENT_FALLBACK, + process.env.NODE_ENV !== 'production' + ? `There is no \`timeZone\` configured, this can lead to markup mismatches caused by environment differences. Consider adding a global default: https://next-intl-docs.vercel.app/docs/configuration#time-zone` + : undefined + ) + ); + } + const translate = useMemo( () => createBaseTranslator({ diff --git a/packages/use-intl/test/core/createIntl.test.tsx b/packages/use-intl/test/core/createIntl.test.tsx deleted file mode 100644 index 363413d63..000000000 --- a/packages/use-intl/test/core/createIntl.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import {parseISO} from 'date-fns'; -import {it, expect} from 'vitest'; -import {createIntl} from '../../src'; - -const intl = createIntl({locale: 'en', timeZone: 'Europe/Berlin'}); - -it('formats a date and time', () => { - expect( - intl.formatDateTime(parseISO('2020-11-20T10:36:01.516Z'), { - dateStyle: 'medium' - }) - ).toBe('Nov 20, 2020'); -}); - -it('formats a number', () => { - expect( - intl.formatNumber(123456.789, {style: 'currency', currency: 'USD'}) - ).toBe('$123,456.79'); -}); - -it('formats a relative time', () => { - expect( - intl.formatRelativeTime( - parseISO('2020-11-20T10:36:01.516Z'), - parseISO('2020-11-20T12:30:01.516Z') - ) - ).toBe('2 hours ago'); -}); diff --git a/packages/use-intl/test/react/useIntl.test.tsx b/packages/use-intl/test/react/useIntl.test.tsx deleted file mode 100644 index 8a9ffadb7..000000000 --- a/packages/use-intl/test/react/useIntl.test.tsx +++ /dev/null @@ -1,438 +0,0 @@ -import {render, screen} from '@testing-library/react'; -import {parseISO} from 'date-fns'; -import React, {ComponentProps, ReactNode} from 'react'; -import {it, expect, describe, vi} from 'vitest'; -import { - DateTimeFormatOptions, - NumberFormatOptions, - IntlError, - IntlErrorCode, - IntlProvider, - useIntl -} from '../../src'; - -function MockProvider( - props: Partial> & {children: ReactNode} -) { - return ( - - ); -} - -describe('formatDateTime', () => { - const mockDate = parseISO('2020-11-20T10:36:01.516Z'); - - function renderDateTime( - value: Date | number, - options?: DateTimeFormatOptions - ) { - function Component() { - const intl = useIntl(); - return <>{intl.formatDateTime(value, options)}; - } - - render( - - - - ); - } - - it('formats a date', () => { - renderDateTime(mockDate); - screen.getByText('11/20/2020'); - }); - - it('formats a time', () => { - renderDateTime(mockDate, {minute: 'numeric', hour: 'numeric'}); - screen.getByText('11:36 AM'); - }); - - it('accepts options', () => { - renderDateTime(mockDate, {month: 'long'}); - screen.getByText('November'); - }); - - it('formats time', () => { - renderDateTime(mockDate, {hour: 'numeric', minute: 'numeric'}); - screen.getByText('11:36 AM'); - }); - - it('can use a global date format', () => { - function Component() { - const intl = useIntl(); - return <>{intl.formatDateTime(mockDate, 'onlyYear')}; - } - - render( - - - - ); - - screen.getByText('2020'); - }); - - it('can use a global time format', () => { - function Component() { - const intl = useIntl(); - return <>{intl.formatDateTime(mockDate, 'onlyHours')}; - } - - render( - - - - ); - - screen.getByText('11 AM'); - }); - - describe('time zones', () => { - it('converts a date to the target time zone', () => { - renderDateTime(mockDate, { - timeZone: 'Asia/Shanghai', - hour: 'numeric', - minute: 'numeric' - }); - screen.getByText('6:36 PM'); - }); - - it('can use a global time zone', () => { - function Component() { - const intl = useIntl(); - return ( - <> - {intl.formatDateTime(mockDate, { - hour: 'numeric', - minute: 'numeric' - })} - - ); - } - - render( - - - - ); - - screen.getByText('6:36 PM'); - }); - - it('can override a global time zone', () => { - function Component() { - const intl = useIntl(); - return ( - <> - {intl.formatDateTime(mockDate, { - timeZone: 'Australia/Sydney', - hour: 'numeric', - minute: 'numeric' - })} - - ); - } - - render( - - - - ); - - screen.getByText('9:36 PM'); - }); - }); - - describe('error handling', () => { - it('handles missing formats', () => { - const onError = vi.fn(); - - function Component() { - const intl = useIntl(); - return <>{intl.formatDateTime(mockDate, 'onlyYear')}; - } - - const {container} = render( - - - - ); - - const error: IntlError = onError.mock.calls[0][0]; - expect(error.message).toBe( - 'MISSING_FORMAT: Format `onlyYear` is not available. You can configure it on the provider or provide custom options.' - ); - expect(error.code).toBe(IntlErrorCode.MISSING_FORMAT); - expect(container.textContent).toMatch(/Nov 20 2020/); - }); - - it('handles formatting errors', () => { - const onError = vi.fn(); - - function Component() { - const intl = useIntl(); - - // @ts-expect-error - return <>{intl.formatDateTime(mockDate, {year: 'very long'})}; - } - - const {container} = render( - - - - ); - - const error: IntlError = onError.mock.calls[0][0]; - expect(error.message).toBe( - 'FORMATTING_ERROR: Value very long out of range for Intl.DateTimeFormat options property year' - ); - expect(error.code).toBe(IntlErrorCode.FORMATTING_ERROR); - expect(container.textContent).toMatch(/Nov 20 2020/); - }); - }); -}); - -describe('formatNumber', () => { - function renderNumber(value: number, options?: NumberFormatOptions) { - function Component() { - const intl = useIntl(); - return <>{intl.formatNumber(value, options)}; - } - - render( - - - - ); - } - - it('formats a number', () => { - renderNumber(2948192329.12312); - screen.getByText('2,948,192,329.123'); - }); - - it('accepts options', () => { - renderNumber(299.99, {currency: 'EUR', style: 'currency'}); - screen.getByText('€299.99'); - }); - - it('can use a global format', () => { - function Component() { - const intl = useIntl(); - return <>{intl.formatNumber(10000, 'noGrouping')}; - } - - render( - - - - ); - - screen.getByText('10000'); - }); - - describe('error handling', () => { - const mockNumber = 10000; - - it('handles missing formats', () => { - const onError = vi.fn(); - - function Component() { - const intl = useIntl(); - return <>{intl.formatNumber(mockNumber, 'missing')}; - } - - const {container} = render( - - - - ); - - const error: IntlError = onError.mock.calls[0][0]; - expect(error.message).toBe( - 'MISSING_FORMAT: Format `missing` is not available. You can configure it on the provider or provide custom options.' - ); - expect(error.code).toBe(IntlErrorCode.MISSING_FORMAT); - expect(container.textContent).toBe('10000'); - }); - - it('handles formatting errors', () => { - const onError = vi.fn(); - - function Component() { - const intl = useIntl(); - return <>{intl.formatNumber(mockNumber, {currency: 'unknown'})}; - } - - const {container} = render( - - - - ); - - const error: IntlError = onError.mock.calls[0][0]; - expect(error.message).toBe( - 'FORMATTING_ERROR: Invalid currency code : unknown' - ); - expect(error.code).toBe(IntlErrorCode.FORMATTING_ERROR); - expect(container.textContent).toBe('10000'); - }); - }); -}); - -describe('formatRelativeTime', () => { - function renderNumber(date: Date | number, now: Date | number) { - function Component() { - const intl = useIntl(); - return <>{intl.formatRelativeTime(date, now)}; - } - - render( - - - - ); - } - - it('can format now', () => { - renderNumber( - parseISO('2020-11-20T10:36:00.000Z'), - parseISO('2020-11-20T10:36:00.100Z') - ); - screen.getByText('now'); - }); - - it('can format seconds', () => { - renderNumber( - parseISO('2020-11-20T10:35:31.000Z'), - parseISO('2020-11-20T10:36:00.000Z') - ); - screen.getByText('29 seconds ago'); - }); - - it('can format minutes', () => { - renderNumber( - parseISO('2020-11-20T10:12:00.000Z'), - parseISO('2020-11-20T10:36:00.000Z') - ); - screen.getByText('24 minutes ago'); - }); - - it('uses the lowest unit possible', () => { - renderNumber( - parseISO('2020-11-20T09:37:00.000Z'), - parseISO('2020-11-20T10:36:00.000Z') - ); - screen.getByText('59 minutes ago'); - }); - - it('can format hours', () => { - renderNumber( - parseISO('2020-11-20T08:30:00.000Z'), - parseISO('2020-11-20T10:36:00.000Z') - ); - screen.getByText('2 hours ago'); - }); - - it('can format days', () => { - renderNumber( - parseISO('2020-11-17T10:36:00.000Z'), - parseISO('2020-11-20T10:36:00.000Z') - ); - screen.getByText('3 days ago'); - }); - - it('can format weeks', () => { - renderNumber( - parseISO('2020-11-02T10:36:00.000Z'), - parseISO('2020-11-20T10:36:00.000Z') - ); - screen.getByText('3 weeks ago'); - }); - - it('can format months', () => { - renderNumber( - parseISO('2020-03-02T10:36:00.000Z'), - parseISO('2020-11-20T10:36:00.000Z') - ); - screen.getByText('9 months ago'); - }); - - it('can format years', () => { - renderNumber( - parseISO('1984-11-20T10:36:00.000Z'), - parseISO('2020-11-20T10:36:00.000Z') - ); - screen.getByText('36 years ago'); - }); - - it('can use a global `now` fallback', () => { - function Component() { - const intl = useIntl(); - const mockDate = parseISO('1984-11-20T10:36:00.000Z'); - return <>{intl.formatRelativeTime(mockDate)}; - } - - render( - - - - ); - - screen.getByText('34 years ago'); - }); - - describe('error handling', () => { - it('handles formatting errors', () => { - const onError = vi.fn(); - - function Component() { - const intl = useIntl(); - // @ts-expect-error Provoke an error - const date = 'not a number' as number; - return <>{intl.formatRelativeTime(date, -20)}; - } - - const {container} = render( - - - - ); - - const error: IntlError = onError.mock.calls[0][0]; - expect(error.message).toBe( - 'FORMATTING_ERROR: Value need to be finite number for Intl.RelativeTimeFormat.prototype.format()' - ); - expect(error.code).toBe(IntlErrorCode.FORMATTING_ERROR); - expect(container.textContent).toBe('not a number'); - }); - - it('throws when no `now` value is available', () => { - const onError = vi.fn(); - - function Component() { - const intl = useIntl(); - const mockDate = parseISO('1984-11-20T10:36:00.000Z'); - return <>{intl.formatRelativeTime(mockDate)}; - } - - render( - - - - ); - - 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.code).toBe(IntlErrorCode.ENVIRONMENT_FALLBACK); - }); - }); -});