diff --git a/docs/pages/blog/next-intl-3-0.mdx b/docs/pages/blog/next-intl-3-0.mdx index 048c1e0d0..f79ffe703 100644 --- a/docs/pages/blog/next-intl-3-0.mdx +++ b/docs/pages/blog/next-intl-3-0.mdx @@ -15,7 +15,7 @@ If you're still happy with the Pages Router, rest assured that `next-intl` is de ## New features 1. **Support for React Server Components**: The APIs `useTranslations`, `useFormatter`, `useLocale`, `useNow` and `useTimeZone` can now be used in Server Components ([proposed docs](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/environments/server-client-components)). -2. **New async APIs to handle i18n outside of components**: To handle i18n in the Metadata API and Route Handlers, the APIs `getTranslator`, `getFormatter`, `getNow`, and `getTimeZone` have been added ([proposed docs](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/environments/metadata-route-handlers)). +2. **New async APIs to handle i18n outside of components**: To handle i18n in the Metadata API and Route Handlers, the APIs `getTranslations`, `getFormatter`, `getNow`, and `getTimeZone` have been added ([proposed docs](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/environments/metadata-route-handlers)). 3. **Middleware for internationalized routing**: While Next.js has built-in support for this with the Pages Router, the App Router doesn't include a built-in solution anymore. `next-intl` now provides a drop-in solution that has you covered ([proposed docs](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/routing/middleware)). 4. **Internationalized navigation APIs**: Similar to the middleware, this provides a drop-in solution that adds internationalization support for Next.js' navigation APIs: `Link`, `useRouter`, `usePathname` and `redirect`. These APIs allow you to handle locale prefixes behind the scenes and also provide support for localizing pathnames (e.g. `/en/about` vs. `/de/ueber-uns`, see the [proposed docs](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/routing/navigation)). diff --git a/docs/pages/blog/translations-outside-of-react-components.mdx b/docs/pages/blog/translations-outside-of-react-components.mdx index a84abc5d9..608337aee 100644 --- a/docs/pages/blog/translations-outside-of-react-components.mdx +++ b/docs/pages/blog/translations-outside-of-react-components.mdx @@ -116,13 +116,13 @@ If you’re working with Next.js, you might want to translate i18n messages in [ `next-intl/server` provides a set of awaitable versions of the functions that you usually call as hooks from within components. These are agnostic from React and can be used for these cases. ```tsx -import {getTranslator} from 'next-intl/server'; +import {getTranslations} from 'next-intl/server'; // The `locale` is received from Next.js via `params` const locale = params.locale; // This creates the same function that is returned by `useTranslations`. -const t = await getTranslator(locale); +const t = await getTranslations(locale); // Result: "Hello world!" t('hello', {name: 'world'}); diff --git a/docs/pages/docs/environments/metadata-route-handlers.mdx b/docs/pages/docs/environments/metadata-route-handlers.mdx index 96241b91d..1924078d4 100644 --- a/docs/pages/docs/environments/metadata-route-handlers.mdx +++ b/docs/pages/docs/environments/metadata-route-handlers.mdx @@ -9,42 +9,17 @@ 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) -`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 { - getTranslator, - getFormatter, - getNow, - getTimeZone, - getMessages -} from 'next-intl/server'; - -// The `locale` is received from Next.js via `params` -const locale = params.locale; - -const t = await getTranslator(locale, 'Metadata'); -const format = await getFormatter(locale); -const now = await getNow(locale); -const timeZone = await getTimeZone(locale); -const messages = await getMessages(locale); -``` - - - 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. - +`next-intl/server` provides a set of [awaitable functions](/docs/environments/server-client-components#async-components) that can be used in these cases. ### 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 {getTranslator} from 'next-intl/server'; +import {getTranslations} from 'next-intl/server'; export async function generateMetadata({params: {locale}}) { - const t = await getTranslator(locale, 'Metadata'); + const t = await getTranslations({locale, namespace: 'Metadata'}); return { title: t('title') @@ -58,27 +33,28 @@ If you need to internationalize content within [metadata files](https://nextjs.o ```tsx filename="app/[locale]/opengraph-image.tsx" import {ImageResponse} from 'next/og'; -import {getTranslator} from 'next-intl/server'; +import {getTranslations} from 'next-intl/server'; -export default async function Image({params: {locale}}) { - const t = await getTranslator(locale, 'OpenGraph'); +export default async function OpenGraphImage({params: {locale}}) { + const t = await getTranslations({locale, namespace: 'OpenGraphImage'}); return new ImageResponse(
{t('title')}
); } ``` ### Route Handlers -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. +You can use `next-intl` in [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) too. The `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/api/hello/route.tsx" import {NextResponse} from 'next/server'; -import {getTranslator} from 'next-intl/server'; +import {getTranslations} from 'next-intl/server'; export async function GET(request) { + // Example: Receive the `locale` via a search param const {searchParams} = new URL(request.url); const locale = searchParams.get('locale'); - const t = await getTranslator(locale, 'Hello'); + const t = await getTranslations({locale, namespace: 'Hello'}); return NextResponse.json({title: t('title')}); } ``` diff --git a/docs/pages/docs/environments/server-client-components.mdx b/docs/pages/docs/environments/server-client-components.mdx index 65ab1841d..e59b04842 100644 --- a/docs/pages/docs/environments/server-client-components.mdx +++ b/docs/pages/docs/environments/server-client-components.mdx @@ -1,8 +1,8 @@ import Callout from 'components/Callout'; -# Internationalization of Server & Client Components in Next.js 13 +# Internationalization of Server & Client 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. +[React Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components) allow you to implement components that remain server-side only if they don’t require React’s interactive features, such as `useState` and `useEffect`. This applies to handling internationalization too. @@ -10,7 +10,7 @@ This applies to handling internationalization too. import {useTranslations} from 'next-intl'; // Since this component doesn't use any interactive features -// from React, it can be implemented as a Server Component. +// from React, it can be run as a Server Component. export default function Index() { const t = useTranslations('Index'); @@ -18,63 +18,103 @@ export default function Index() { } ``` -Depending on if you import `useTranslations`, `useFormatter`, `useLocale`, `useNow` and `useTimeZone` from a Server or Client Component, `next-intl` will automatically provide an implementation that works best for the given environment. +Moving internationalization to the server side unlocks new levels of performance, leaving the client side for interactive features. -
-Deep dive: How does the Server Components integration work? +**Benefits of server-side internationalization:** -`next-intl` uses [`react-server` conditional exports](https://github.com/reactjs/rfcs/blob/main/text/0227-server-module-conventions.md#react-server-conditional-exports) to load code that is optimized for the usage in Server or Client Components. While configuration for hooks like `useTranslations` is read via `useContext` on the client side, on the server side it is loaded via [`i18n.ts`](/docs/usage/configuration#i18nts). +1. Your messages never leave the server and don't need to be serialized for the client side +2. Library code for internationalization doesn't need to be loaded on the client side +3. No need to split your messages, e.g. based on routes or components +4. No runtime cost on the client side +5. No need to handle environment differences like different time zones on the server and client + +## Using internationalization in Server Components + +Server Components can be declared in two ways: + +1. Async components +2. Non-async, regular components + +In a typical app, you'll likely find both types of components. `next-intl` provides corresponding APIs that work for the given component type. + +### Async components -Hooks are currently primarly known for being used in Client Components since they are typically stateful or don't apply to a server environment. However, hooks like [`useId`](https://react.dev/reference/react/useId) can be used in Server Components too. Similarly, `next-intl` uses a hooks-based API that looks identical, regardless of if it's used in a Server or Client Component. This allows to use hooks like `useTranslations` in [shared components](https://github.com/reactjs/rfcs/blob/bf51f8755ddb38d92e23ad415fc4e3c02b95b331/text/0000-server-components.md#sharing-code-between-server-and-client), which can run both as a Server or a Client Component, depending on where they are imported from. +These are primarly concerned with fetching data and [can not use hooks](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#capabilities--constraints-of-server-and-client-components). Due to this, `next-intl` provides a set of awaitable versions of the functions that you usually call as hooks from within components. -The one restriction that currently comes with this pattern is that hooks can not be called from `async` components. To resolve this, -you can split your component into two, leaving the async code in the first one and -moving the usage of the hook to the second one. +```tsx filename="[locale]/profile/page.tsx" +import {getTranslations} from 'next-intl/server'; -**Example:** +export default async function ProfilePage() { + const user = await fetchUser(); + const t = await getTranslations('ProfilePage'); -```tsx filename="app/[locale]/profile/page.tsx" -export default async function Profile() { - // Use this component for all async code ... - const user = await getUser(); - return ; + return ( + + + + ); } +``` + +These functions are available: + +```tsx +import { + getTranslations, + getFormatter, + getNow, + getTimeZone, + getMessages, + getLocale +} from 'next-intl/server'; + +const t = await getTranslations('ProfilePage'); +const format = await getFormatter(); +const now = await getNow(); +const timeZone = await getTimeZone(); +const messages = await getMessages(); +``` + +### Non-async components [#shared-components] + +Components that aren't declared with the `async` keyword and don't use interactive features like `useState`, are referred to as [shared components](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#sharing-code-between-server-and-client). These can render either as a Server or Client Component, depending on where they are imported from. -function ProfileContent({user}) { - // ... and use this one for rendering the fetched data - const t = useTranslations('ProfileContent'); - return

<{t('title', {userName: user.name})}/p> +In Next.js, Server Components are the default, and therefore shared components will typically execute as Server Components. + +```tsx filename="UserDetails.tsx" +import {useTranslations} from 'next-intl'; + +export default function UserDetails({user}) { + const t = useTranslations('UserProfile'); + + return ( +

+

{t('title')}

+

{t('followers', {count: user.numFollowers})}

+
+ ); } ``` -As a benefit, the extracted component now works both in Server as well as Client Components, depending on where it is rendered. +If you import `useTranslations`, `useFormatter`, `useLocale`, `useNow` and `useTimeZone` from a shared component, `next-intl` will automatically provide an implementation that works best for the environment this component executes in (server or client). -For edge cases in server-only components, you can use [awaitable APIs from `next-intl`](/docs/environments/metadata-route-handlers). +
+How does the Server Components integration work? + +`next-intl` uses [`react-server` conditional exports](https://github.com/reactjs/rfcs/blob/main/text/0227-server-module-conventions.md#react-server-conditional-exports) to load code that is optimized for the usage in Server or Client Components. While configuration for hooks like `useTranslations` is read via `useContext` on the client side, on the server side it is loaded via [`i18n.ts`](/docs/usage/configuration#i18nts). + +Hooks are currently primarly known for being used in Client Components since they are typically stateful or don't apply to a server environment. However, hooks like [`useId`](https://react.dev/reference/react/useId) can be used in Server Components too. Similarly, `next-intl` provides a hooks-based API that looks identical, regardless of if it's used in a Server or Client Component. + +The one restriction that currently comes with this pattern is that hooks can not be called from `async` components. `next-intl` therefore provides a separate set of [awaitable APIs](#async-components) for this use case.
-## Benefits of handling i18n in Server Components [#server-components-benefits] +
+Should I use async or non-async functions for my components? -Moving internationalization to the server side unlocks new levels of performance, leaving the client side for interactive features. +If you implement components that qualify as shared components, it can be beneficial to implement them as non-async functions. This allows to use these components either in a server or client environment, making them really flexible. Even if you don't intend to to ever run a particular component on the client side, this compatibility can still be helpful, e.g. for simplified testing. However, there's no need to dogmatically use non-async functions exclusively for handling internationalization—use what fits your app best. - -
    -
  1. - Your messages never leave the server and don't need to be serialized for - the client side -
  2. -
  3. - Library code for internationalization doesn't need to be loaded on the - client side -
  4. -
  5. No need to split your messages, e.g. based on routes or components
  6. -
  7. No runtime cost on the client side
  8. -
  9. - No need to handle environment differences like different time zones on the - server and client -
  10. -
-
+
## Using internationalization in Client Components @@ -211,8 +251,8 @@ export default async function LocaleLayout({children, params: {locale}}) { ``` - Note that this is a tradeoff in regard to performance (see [the bullet points - above](#server-components-benefits)). + Note that this is a tradeoff in regard to performance (see the bullet points + at the top of this page). ## Troubleshooting diff --git a/docs/pages/docs/getting-started/app-router.mdx b/docs/pages/docs/getting-started/app-router.mdx index 2a26cf865..c4b570a31 100644 --- a/docs/pages/docs/getting-started/app-router.mdx +++ b/docs/pages/docs/getting-started/app-router.mdx @@ -1,9 +1,9 @@ import Callout from 'components/Callout'; import Steps from 'components/Steps'; -# Next.js 13: Internationalization (i18n) +# Next.js App Router 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 on the server side. +Next.js 13 introduces support for [React Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components) with the App Router and unlocks [many benefits](/docs/environments/server-client-components) when handling internationalization on the server side. ## Getting started @@ -155,11 +155,17 @@ That's all it takes!
  • Exploring `next-intl`? Check out the [usage guide](/docs/usage).
  • Ran into an issue? Have a look at [the App Router - example](https://next-intl-example-next-13.vercel.app/) - ([source](https://github.com/amannn/next-intl/tree/main/examples/example-next-13)). + example](https://next-intl-example-next-13.vercel.app/). +
  • +
  • + Want to learn more about about using translations across the server and + client? Check out [the Server & Client Components + guide](/docs/environments/server-client-components). +
  • +
  • + Wondering how to link between internationalized pages? Have a look at [the + navigation docs](/docs/routing/navigation).
  • -
  • Considering using `next-intl` in Client Components? Check out [the Client Components guide](/docs/environments/server-client-components).
  • -
  • Wondering how to link between internationalized pages? Have a look at [the navigation docs](/docs/routing/navigation).
  • @@ -215,7 +221,7 @@ export default function IndexPage({ }) { unstable_setRequestLocale(locale); - // Once the request locale is set, you + // Once the request locale is set, you // can call hooks from `next-intl` const t = useTranslations('IndexPage'); @@ -227,8 +233,6 @@ export default function IndexPage({ **Important:** `unstable_setRequestLocale` needs to be called after the `locale` is validated, but before you call any hooks from `next-intl`. Otherwise, you'll get an error when trying to prerender the page. - -
    What does "unstable" mean? @@ -237,8 +241,8 @@ export default function IndexPage({ 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 `privacy/page.tsx`. That being said, the API is expected to work reliably if you're cautious to apply it in all relevant places. -
    +
    How does unstable_setRequestLocale work? @@ -248,3 +252,23 @@ That being said, the API is expected to work reliably if you're cautious to appl Note that the store is scoped to a request and therefore doesn't affect other requests that might be handled in parallel while a given request resolves asynchronously.
    + +### Use the `locale` param in metadata + +In addition to the rendering of your pages, also page metadata needs to qualify for static rendering. + +To achieve this, you can forward the `locale` that you receive from Next.js via `params` to [the awaitable functions from `next-intl`](/docs/environments/server-client-components#async-components). + +```tsx filename="page.tsx" +import {getTranslations} from 'next-intl/server'; + +export async function generateMetadata({params: {locale}}) { + const t = await getTranslations({locale, namespace: 'Metadata'}); + + return { + title: t('title') + }; +} +``` + + diff --git a/examples/example-next-13-advanced/package.json b/examples/example-next-13-advanced/package.json index c75c520fd..db58c4528 100644 --- a/examples/example-next-13-advanced/package.json +++ b/examples/example-next-13-advanced/package.json @@ -27,6 +27,7 @@ "eslint": "^8.46.0", "eslint-config-molindo": "7.0.0-alpha.7", "eslint-config-next": "^13.4.0", + "sharp": "^0.32.6", "typescript": "^5.0.0" } } diff --git a/examples/example-next-13-advanced/src/app/[locale]/api/route.ts b/examples/example-next-13-advanced/src/app/[locale]/api/route.ts index 045117cff..921971072 100644 --- a/examples/example-next-13-advanced/src/app/[locale]/api/route.ts +++ b/examples/example-next-13-advanced/src/app/[locale]/api/route.ts @@ -1,5 +1,5 @@ import {NextRequest, NextResponse} from 'next/server'; -import {getTranslator} from 'next-intl/server'; +import {getTranslations} from 'next-intl/server'; type Props = { params: { @@ -13,6 +13,6 @@ export async function GET(request: NextRequest, {params: {locale}}: Props) { return new Response('Search param `name` was not provided.', {status: 400}); } - const t = await getTranslator(locale, 'ApiRoute'); + const t = await getTranslations({locale, namespace: 'ApiRoute'}); return NextResponse.json({message: t('hello', {name})}); } diff --git a/examples/example-next-13-advanced/src/app/[locale]/layout.tsx b/examples/example-next-13-advanced/src/app/[locale]/layout.tsx index f9ad554bf..559db61ae 100644 --- a/examples/example-next-13-advanced/src/app/[locale]/layout.tsx +++ b/examples/example-next-13-advanced/src/app/[locale]/layout.tsx @@ -5,7 +5,7 @@ import { getFormatter, getNow, getTimeZone, - getTranslator + getTranslations } from 'next-intl/server'; import {ReactNode} from 'react'; import Navigation from '../../components/Navigation'; @@ -18,10 +18,10 @@ type Props = { export async function generateMetadata({ params: {locale} }: Omit): Promise { - const t = await getTranslator(locale, 'LocaleLayout'); - const formatter = await getFormatter(locale); - const now = await getNow(locale); - const timeZone = await getTimeZone(locale); + const t = await getTranslations({locale, namespace: 'LocaleLayout'}); + const formatter = await getFormatter({locale}); + const now = await getNow({locale}); + const timeZone = await getTimeZone({locale}); return { title: t('title'), diff --git a/examples/example-next-13-advanced/src/app/[locale]/opengraph-image.tsx b/examples/example-next-13-advanced/src/app/[locale]/opengraph-image.tsx index 05e520749..345cbb46f 100644 --- a/examples/example-next-13-advanced/src/app/[locale]/opengraph-image.tsx +++ b/examples/example-next-13-advanced/src/app/[locale]/opengraph-image.tsx @@ -1,5 +1,5 @@ import {ImageResponse} from 'next/server'; -import {getTranslator} from 'next-intl/server'; +import {getTranslations} from 'next-intl/server'; type Props = { params: { @@ -8,6 +8,6 @@ type Props = { }; export default async function Image({params: {locale}}: Props) { - const t = await getTranslator(locale, 'OpenGraph'); + const t = await getTranslations({locale, namespace: 'OpenGraph'}); return new ImageResponse(
    {t('title')}
    ); } diff --git a/examples/example-next-13-advanced/src/app/[locale]/page.tsx b/examples/example-next-13-advanced/src/app/[locale]/page.tsx index 29c34716e..e8197b92d 100644 --- a/examples/example-next-13-advanced/src/app/[locale]/page.tsx +++ b/examples/example-next-13-advanced/src/app/[locale]/page.tsx @@ -12,10 +12,9 @@ import {Link} from '../../navigation'; type Props = { searchParams: Record; - params: {locale: string}; }; -export default function Index({params, searchParams}: Props) { +export default function Index({searchParams}: Props) { const t = useTranslations('Index'); const format = useFormatter(); const now = useNow(); @@ -55,7 +54,7 @@ export default function Index({params, searchParams}: Props) {

    {JSON.stringify(searchParams, null, 2)}

    {/* @ts-ignore -- Waiting for TS support */} - + ); } diff --git a/examples/example-next-13-advanced/src/components/AsyncComponent.tsx b/examples/example-next-13-advanced/src/components/AsyncComponent.tsx index 4cf51960e..bf85ed724 100644 --- a/examples/example-next-13-advanced/src/components/AsyncComponent.tsx +++ b/examples/example-next-13-advanced/src/components/AsyncComponent.tsx @@ -1,11 +1,7 @@ -import {getTranslator} from 'next-intl/server'; +import {getTranslations} from 'next-intl/server'; -type Props = { - locale: string; -}; - -export default async function AsyncComponent({locale}: Props) { - const t = await getTranslator(locale, 'AsyncComponent'); +export default async function AsyncComponent() { + const t = await getTranslations('AsyncComponent'); return (
    diff --git a/examples/example-next-13-advanced/src/components/client/02-MessagesOnClientCounter/Counter.tsx b/examples/example-next-13-advanced/src/components/client/02-MessagesOnClientCounter/Counter.tsx index 980b495c4..4e3a922be 100644 --- a/examples/example-next-13-advanced/src/components/client/02-MessagesOnClientCounter/Counter.tsx +++ b/examples/example-next-13-advanced/src/components/client/02-MessagesOnClientCounter/Counter.tsx @@ -4,7 +4,6 @@ import ClientCounter from './ClientCounter'; export default function Counter() { const messages = useMessages(); - if (!messages) return null; return ( ) { - const t = await getTranslator(locale, 'LocaleLayout'); + const t = await getTranslations({locale, namespace: 'LocaleLayout'}); return { title: t('title') diff --git a/packages/next-intl/src/react-server/useFormatter.tsx b/packages/next-intl/src/react-server/useFormatter.tsx index 30230ef58..4001d0e64 100644 --- a/packages/next-intl/src/react-server/useFormatter.tsx +++ b/packages/next-intl/src/react-server/useFormatter.tsx @@ -8,5 +8,5 @@ export default function useFormatter( ...[]: Parameters ): ReturnType { const locale = useLocale(); - return useHook('useFormatter', getFormatter(locale)); + return useHook('useFormatter', getFormatter({locale})); } diff --git a/packages/next-intl/src/react-server/useHook.tsx b/packages/next-intl/src/react-server/useHook.tsx index 4c0637d5d..5aeb5b1c3 100644 --- a/packages/next-intl/src/react-server/useHook.tsx +++ b/packages/next-intl/src/react-server/useHook.tsx @@ -11,35 +11,8 @@ export default function useHook( error instanceof TypeError && error.message.includes("Cannot read properties of null (reading 'use')") ) { - const asyncAlternative = { - // useLocale: No alternative needed - useTranslations: 'getTranslator', - useFormatter: 'getFormatter', - useNow: 'getNow', - useTimeZone: 'getTimeZone', - useMessages: 'getMessages' - }[hookName]; - throw new Error( - `\`${hookName}\` is not callable within an async component. To resolve this, you can split your component into two, leaving the async code in the first one and moving the usage of \`${hookName}\` to the second one. - -Example: - -async function Profile() { - const user = await getUser(); - return ; -} - -function ProfileContent({user}) { - // Call \`${hookName}\` here and use the \`user\` prop - return ...; -} - -This allows you to use the extracted component both in Server as well as Client Components, depending on where it's imported from.${ - asyncAlternative - ? `\n\nFor edge cases in server-only components, you can use the \`await ${asyncAlternative}(locale)\` API from 'next-intl/server' instead.` - : '' - }`, + `\`${hookName}\` is not callable within an async component. Please refer to https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/environments/server-client-components#async-components`, {cause: error} ); } else { diff --git a/packages/next-intl/src/react-server/useMessages.tsx b/packages/next-intl/src/react-server/useMessages.tsx index 2f5eba203..bf9282380 100644 --- a/packages/next-intl/src/react-server/useMessages.tsx +++ b/packages/next-intl/src/react-server/useMessages.tsx @@ -8,5 +8,5 @@ export default function useMessages( ...[]: Parameters ): ReturnType { const locale = useLocale(); - return useHook('useMessages', getMessages(locale)); + return useHook('useMessages', getMessages({locale})); } diff --git a/packages/next-intl/src/react-server/useNow.tsx b/packages/next-intl/src/react-server/useNow.tsx index 4359126cf..289b73c4f 100644 --- a/packages/next-intl/src/react-server/useNow.tsx +++ b/packages/next-intl/src/react-server/useNow.tsx @@ -13,5 +13,5 @@ export default function useNow( } const locale = useLocale(); - return useHook('useNow', getNow(locale)); + return useHook('useNow', getNow({locale})); } diff --git a/packages/next-intl/src/react-server/useTimeZone.tsx b/packages/next-intl/src/react-server/useTimeZone.tsx index a9907a4cd..4b5e4508a 100644 --- a/packages/next-intl/src/react-server/useTimeZone.tsx +++ b/packages/next-intl/src/react-server/useTimeZone.tsx @@ -8,5 +8,5 @@ export default function useTimeZone( ...[]: Parameters ): ReturnType { const locale = useLocale(); - return useHook('useTimeZone', getTimeZone(locale)); + return useHook('useTimeZone', getTimeZone({locale})); } diff --git a/packages/next-intl/src/server/getFormatter.tsx b/packages/next-intl/src/server/getFormatter.tsx index 371f01708..cb16a9e5d 100644 --- a/packages/next-intl/src/server/getFormatter.tsx +++ b/packages/next-intl/src/server/getFormatter.tsx @@ -1,6 +1,12 @@ import {cache} from 'react'; import {createFormatter} from 'use-intl/core'; import getConfig from './getConfig'; +import resolveLocaleArg from './resolveLocaleArg'; + +const getFormatterImpl = cache(async (locale: string) => { + const config = await getConfig(locale); + return createFormatter(config); +}); /** * Returns a formatter based on the given locale. @@ -8,9 +14,7 @@ import getConfig from './getConfig'; * The formatter automatically receives the request config, but * you can override it by passing in additional options. */ -const getFormatter = cache(async (locale: string) => { - const config = await getConfig(locale); - return createFormatter(config); -}); - -export default getFormatter; +export default function getFormatter(opts?: {locale?: string} | string) { + const locale = resolveLocaleArg('getFormatter', opts); + return getFormatterImpl(locale); +} diff --git a/packages/next-intl/src/server/getLocale.tsx b/packages/next-intl/src/server/getLocale.tsx new file mode 100644 index 000000000..5e9108515 --- /dev/null +++ b/packages/next-intl/src/server/getLocale.tsx @@ -0,0 +1,5 @@ +import {getRequestLocale} from './RequestLocale'; + +export default function getLocale() { + return getRequestLocale(); +} diff --git a/packages/next-intl/src/server/getMessages.tsx b/packages/next-intl/src/server/getMessages.tsx index 28fb7ed3e..101255776 100644 --- a/packages/next-intl/src/server/getMessages.tsx +++ b/packages/next-intl/src/server/getMessages.tsx @@ -1,8 +1,10 @@ import {cache} from 'react'; import getConfig from './getConfig'; +import getLocale from './getLocale'; +import resolveLocaleArg from './resolveLocaleArg'; -const getMessages = cache(async (locale: string) => { - const config = await getConfig(locale); +const getMessagesImpl = cache(async (locale?: string) => { + const config = await getConfig(locale || getLocale()); if (!config.messages) { throw new Error( @@ -13,4 +15,7 @@ const getMessages = cache(async (locale: string) => { return config.messages; }); -export default getMessages; +export default function getMessages(opts?: {locale?: string} | string) { + const locale = resolveLocaleArg('getMessages', opts); + return getMessagesImpl(locale); +} diff --git a/packages/next-intl/src/server/getNow.tsx b/packages/next-intl/src/server/getNow.tsx index 97c068000..4ab0be2b1 100644 --- a/packages/next-intl/src/server/getNow.tsx +++ b/packages/next-intl/src/server/getNow.tsx @@ -1,9 +1,13 @@ import {cache} from 'react'; import getConfig from './getConfig'; +import resolveLocaleArg from './resolveLocaleArg'; -const getNow = cache(async (locale: string) => { +const getNowImpl = cache(async (locale: string) => { const config = await getConfig(locale); return config.now; }); -export default getNow; +export default function getNow(opts?: {locale?: string} | string) { + const locale = resolveLocaleArg('getNow', opts); + return getNowImpl(locale); +} diff --git a/packages/next-intl/src/server/getTimeZone.tsx b/packages/next-intl/src/server/getTimeZone.tsx index 5160495a0..c96952131 100644 --- a/packages/next-intl/src/server/getTimeZone.tsx +++ b/packages/next-intl/src/server/getTimeZone.tsx @@ -1,9 +1,13 @@ import {cache} from 'react'; import getConfig from './getConfig'; +import resolveLocaleArg from './resolveLocaleArg'; -const getTimeZone = cache(async (locale: string) => { +const getTimeZoneImpl = cache(async (locale: string) => { const config = await getConfig(locale); return config.timeZone; }); -export default getTimeZone; +export default function getTimeZone(opts?: {locale?: string} | string) { + const locale = resolveLocaleArg('getTimeZone', opts); + return getTimeZoneImpl(locale); +} diff --git a/packages/next-intl/src/server/getTranslations.tsx b/packages/next-intl/src/server/getTranslations.tsx new file mode 100644 index 000000000..cf4af24cf --- /dev/null +++ b/packages/next-intl/src/server/getTranslations.tsx @@ -0,0 +1,124 @@ +import {ReactElement, ReactNodeArray, cache} from 'react'; +import { + createTranslator, + Formats, + TranslationValues, + MessageKeys, + NamespaceKeys, + NestedKeyOf, + NestedValueOf, + RichTranslationValues, + MarkupTranslationValues +} from 'use-intl/core'; +import getConfig from './getConfig'; +import getLocale from './getLocale'; + +async function getTranslations< + NestedKey extends NamespaceKeys< + IntlMessages, + NestedKeyOf + > = never +>( + namespaceOrOpts?: NestedKey | {locale: string; 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?: RichTranslationValues, + formats?: Partial + ): string | ReactElement | ReactNodeArray; + + // `markup` + markup< + TargetKey extends MessageKeys< + NestedValueOf< + {'!': IntlMessages}, + [NestedKey] extends [never] ? '!' : `!.${NestedKey}` + >, + NestedKeyOf< + NestedValueOf< + {'!': IntlMessages}, + [NestedKey] extends [never] ? '!' : `!.${NestedKey}` + > + > + > + >( + key: TargetKey, + values?: MarkupTranslationValues, + formats?: Partial + ): string; + + // `raw` + raw< + TargetKey extends MessageKeys< + NestedValueOf< + {'!': IntlMessages}, + [NestedKey] extends [never] ? '!' : `!.${NestedKey}` + >, + NestedKeyOf< + NestedValueOf< + {'!': IntlMessages}, + [NestedKey] extends [never] ? '!' : `!.${NestedKey}` + > + > + > + >( + key: TargetKey + ): any; +}> { + let namespace: NestedKey | undefined, locale: string; + + if (typeof namespaceOrOpts === 'string') { + namespace = namespaceOrOpts; + locale = getLocale(); + } else if (namespaceOrOpts) { + namespace = namespaceOrOpts.namespace; + locale = namespaceOrOpts.locale; + } else { + locale = getLocale(); + } + + const config = await getConfig(locale); + + return createTranslator({ + ...config, + namespace, + messages: config.messages + }); +} + +export default cache(getTranslations); diff --git a/packages/next-intl/src/server/getTranslator.tsx b/packages/next-intl/src/server/getTranslator.tsx index a8b0b4eb5..5d5800487 100644 --- a/packages/next-intl/src/server/getTranslator.tsx +++ b/packages/next-intl/src/server/getTranslator.tsx @@ -1,6 +1,5 @@ import {ReactElement, ReactNodeArray, cache} from 'react'; import { - createTranslator, Formats, TranslationValues, MessageKeys, @@ -10,9 +9,13 @@ import { RichTranslationValues, MarkupTranslationValues } from 'use-intl/core'; -import getConfig from './getConfig'; +import getTranslations from './getTranslations'; -async function getTranslatorImpl< +// TODO: Remove +const getDeprecation = cache(() => ({hasWarned: false})); + +/** @deprecated Deprecated in favor of `getTranslations`. See https://github.com/amannn/next-intl/pull/600 */ +export default async function getTranslator< NestedKey extends NamespaceKeys< IntlMessages, NestedKeyOf @@ -100,13 +103,22 @@ Promise<{ key: TargetKey ): any; }> { - const config = await getConfig(locale); + if (!getDeprecation().hasWarned) { + console.error( + `\nDEPRECATION WARNING: \`getTranslator\` has been deprecated in favor of \`getTranslations\`: - return createTranslator({ - ...config, - namespace, - messages: config.messages - }); -} +import {getTranslations} from 'next-intl/server'; + +// With implicit request locale +const t = await getTranslator('${namespace}'); -export default cache(getTranslatorImpl); +// With explicit locale +const t = await getTranslator({locale: '${locale}', namespace: '${namespace}'}); + +See https://github.com/amannn/next-intl/pull/600\n` + ); + getDeprecation().hasWarned = true; + } + + return getTranslations({locale, namespace}); +} diff --git a/packages/next-intl/src/server/index.tsx b/packages/next-intl/src/server/index.tsx index 484b0bf3a..be2f88348 100644 --- a/packages/next-intl/src/server/index.tsx +++ b/packages/next-intl/src/server/index.tsx @@ -7,7 +7,11 @@ export {default as getRequestConfig} from './getRequestConfig'; export {default as getFormatter} from './getFormatter'; export {default as getNow} from './getNow'; export {default as getTimeZone} from './getTimeZone'; -export {default as getTranslator} from './getTranslator'; +export {default as getTranslations} from './getTranslations'; export {default as getMessages} from './getMessages'; +export {default as getLocale} from './getLocale'; export {setRequestLocale as unstable_setRequestLocale} from './RequestLocale'; + +// TODO: Remove +export {default as getTranslator} from './getTranslator'; diff --git a/packages/next-intl/src/server/react-client/index.tsx b/packages/next-intl/src/server/react-client/index.tsx index f39fdf4cc..6bc0bee18 100644 --- a/packages/next-intl/src/server/react-client/index.tsx +++ b/packages/next-intl/src/server/react-client/index.tsx @@ -4,6 +4,7 @@ import type { getNow as getNow_type, getTimeZone as getTimeZone_type, getTranslator as getTranslator_type, + getTranslations as getTranslations_type, getMessages as getMessages_type, unstable_setRequestLocale as unstable_setRequestLocale_type } from '..'; @@ -29,8 +30,13 @@ 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 Deprecated in favor of `getTranslations`. See https://github.com/amannn/next-intl/pull/600 */ export const getTranslator = notSupported('getTranslator') as unknown as typeof getTranslator_type; // prettier-ignore +export const getTranslations = notSupported('getTranslations') as unknown as typeof getTranslations_type; +// prettier-ignore export const getMessages = notSupported('getMessages') as unknown as typeof getMessages_type; // prettier-ignore +export const getLocale = notSupported('getLocale') as unknown as typeof getMessages_type; +// prettier-ignore export const unstable_setRequestLocale = notSupported('unstable_setRequestLocale') as unknown as typeof unstable_setRequestLocale_type; diff --git a/packages/next-intl/src/server/resolveLocaleArg.tsx b/packages/next-intl/src/server/resolveLocaleArg.tsx new file mode 100644 index 000000000..2634000a9 --- /dev/null +++ b/packages/next-intl/src/server/resolveLocaleArg.tsx @@ -0,0 +1,30 @@ +import {cache} from 'react'; +import getLocale from './getLocale'; + +// TODO: Remove + +const deprecate = cache((fnName: string, locale: string) => { + console.error( + `\nDEPRECATION WARNING: Passing a locale as a string to \`${fnName}\` has been deprecated in favor of passing an object with a \`locale\` property instead: + +${fnName}({locale: '${locale}'}); + +See https://github.com/amannn/next-intl/pull/600\n` + ); +}); + +export default function resolveLocaleArg( + fnName: string, + optsOrDeprecatedLocale?: {locale?: string} | string +) { + if (typeof optsOrDeprecatedLocale === 'string') { + deprecate(fnName, optsOrDeprecatedLocale); + return optsOrDeprecatedLocale; + } + + if (optsOrDeprecatedLocale?.locale) { + return optsOrDeprecatedLocale.locale; + } + + return getLocale(); +} diff --git a/packages/next-intl/test/server/getTranslator.test.tsx b/packages/next-intl/test/server/index.test.tsx similarity index 50% rename from packages/next-intl/test/server/getTranslator.test.tsx rename to packages/next-intl/test/server/index.test.tsx index da8ced41c..53f9c25e9 100644 --- a/packages/next-intl/test/server/getTranslator.test.tsx +++ b/packages/next-intl/test/server/index.test.tsx @@ -2,12 +2,13 @@ import {it, vi, expect, describe} from 'vitest'; import { - getTranslator, + getTranslations, getMessages, getFormatter, getNow, getTimeZone } from '../../src/server.react-server'; +import {HEADER_LOCALE_NAME} from '../../src/shared/constants'; vi.mock('next-intl/config', () => ({ default: async () => @@ -19,6 +20,7 @@ vi.mock('next-intl/config', () => ({ timeZone: 'Europe/London', messages: { About: { + basic: 'Hello', interpolation: 'Hello {name}', rich: '{name}' } @@ -26,6 +28,18 @@ vi.mock('next-intl/config', () => ({ }) })); +vi.mock('next/headers', () => ({ + headers: () => ({ + get(name: string) { + if (name === HEADER_LOCALE_NAME) { + return 'en'; + } else { + throw new Error('Unknown header: ' + name); + } + } + }) +})); + vi.mock('react', async (importOriginal) => { const React = (await importOriginal()) as typeof import('react'); return { @@ -36,14 +50,24 @@ vi.mock('react', async (importOriginal) => { }; }); -describe('getTranslator', () => { +describe('getTranslations', () => { + it('works with an implicit locale', async () => { + const t = await getTranslations('About'); + expect(t('basic')).toBe('Hello'); + }); + + it('works without a namespace', async () => { + const t = await getTranslations(); + expect(t('About.basic')).toBe('Hello'); + }); + it('can interpolate variables', async () => { - const t = await getTranslator('en', 'About'); + const t = await getTranslations({locale: 'en', namespace: 'About'}); expect(t('interpolation', {name: 'Jane'})).toBe('Hello Jane'); }); it('renders rich text to a string', async () => { - const t = await getTranslator('en', 'About'); + const t = await getTranslations({locale: 'en', namespace: 'About'}); expect( t.rich('rich', { name: 'Example', @@ -53,14 +77,21 @@ describe('getTranslator', () => { }); it('renders raw text to a string', async () => { - const t = await getTranslator('en', 'About'); + const t = await getTranslations({locale: 'en', namespace: 'About'}); expect(t.raw('rich')).toBe('{name}'); }); }); describe('getFormatter', () => { + it('works with an implicit locale', async () => { + const format = await getFormatter(); + expect(format.dateTime(new Date('2020-01-01T00:00:00.000Z'))).toBe( + '1/1/2020' + ); + }); + it('can format a date', async () => { - const format = await getFormatter('en'); + const format = await getFormatter({locale: 'en'}); expect(format.dateTime(new Date('2020-01-01T00:00:00.000Z'))).toBe( '1/1/2020' ); @@ -68,19 +99,33 @@ describe('getFormatter', () => { }); describe('getNow', () => { + it('works with an implicit locale', async () => { + expect((await getNow()).toISOString()).toBe('2020-01-01T00:00:00.000Z'); + }); + it('returns the current time', async () => { - expect((await getNow('en')).toISOString()).toBe('2020-01-01T00:00:00.000Z'); + expect((await getNow({locale: 'en'})).toISOString()).toBe( + '2020-01-01T00:00:00.000Z' + ); }); }); describe('getMessages', () => { + it('works with an implicit locale', async () => { + expect(await getMessages()).toHaveProperty('About'); + }); + it('returns the messages', async () => { - expect(await getMessages('en')).toHaveProperty('About'); + expect(await getMessages({locale: 'en'})).toHaveProperty('About'); }); }); describe('getTimeZone', () => { + it('works with an implicit locale', async () => { + expect(await getTimeZone()).toBe('Europe/London'); + }); + it('returns the time zone', async () => { - expect(await getTimeZone('en')).toBe('Europe/London'); + expect(await getTimeZone({locale: 'en'})).toBe('Europe/London'); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index caeb130c6..f85a21b25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -325,6 +325,9 @@ importers: eslint-config-next: specifier: ^13.4.0 version: 13.4.0(eslint@8.46.0)(typescript@5.0.4) + sharp: + specifier: ^0.32.6 + version: 0.32.6 typescript: specifier: ^5.0.0 version: 5.0.4 @@ -8225,6 +8228,10 @@ packages: deep-equal: 2.2.1 dev: true + /b4a@1.6.4: + resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} + dev: true + /babel-core@7.0.0-bridge.0(@babel/core@7.22.5): resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} peerDependencies: @@ -9551,7 +9558,6 @@ packages: dependencies: color-name: 1.1.4 simple-swizzle: 0.2.2 - dev: false /color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} @@ -9565,6 +9571,14 @@ packages: color-string: 1.9.1 dev: false + /color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + dev: true + /colorette@1.4.0: resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} dev: false @@ -10687,7 +10701,6 @@ packages: /deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} - dev: false /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -10873,6 +10886,11 @@ packages: engines: {node: '>=8'} dev: true + /detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + dev: true + /detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -12290,6 +12308,11 @@ packages: - supports-color dev: false + /expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + dev: true + /expect@29.5.0: resolution: {integrity: sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -12546,6 +12569,10 @@ packages: resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} dev: true + /fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + dev: true + /fast-glob@3.2.11: resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==} engines: {node: '>=8.6.0'} @@ -13301,6 +13328,10 @@ packages: ini: 1.3.8 dev: true + /github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + dev: true + /github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} dev: false @@ -14453,7 +14484,6 @@ packages: /is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - dev: false /is-async-function@2.0.0: resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} @@ -17898,6 +17928,10 @@ packages: picocolors: 1.0.0 dev: true + /napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + dev: true + /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} dev: true @@ -18171,6 +18205,13 @@ packages: engines: {node: '>=12.0.0'} dev: false + /node-abi@3.51.0: + resolution: {integrity: sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + dev: true + /node-addon-api@1.7.2: resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} requiresBuild: true @@ -18181,6 +18222,10 @@ packages: resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} dev: true + /node-addon-api@6.1.0: + resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} + dev: true + /node-dir@0.1.17: resolution: {integrity: sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==} engines: {node: '>= 0.10.5'} @@ -20045,6 +20090,25 @@ packages: resolution: {integrity: sha512-q44QFLhOhty2Bd0Y46fnYW0gD/cbVM9dUVtNTDKPcdXSMA7jfY+Jpd6rk3GB0lcQss0z5s/6CmVP0Z/hV+g6pw==} dev: false + /prebuild-install@7.1.1: + resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + detect-libc: 2.0.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.51.0 + pump: 3.0.0 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: true + /prelude-ls@1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} engines: {node: '>= 0.8.0'} @@ -20427,6 +20491,10 @@ packages: /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + /queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + dev: true + /queue@6.0.2: resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} dependencies: @@ -20485,7 +20553,6 @@ packages: ini: 1.3.8 minimist: 1.2.8 strip-json-comments: 2.0.1 - dev: false /react-dev-utils@11.0.4(typescript@5.0.4)(webpack@4.43.0): resolution: {integrity: sha512-dx0LvIGHcOPtKbeiSUM4jqpBl3TcY7CDjZdfOIcKeznE7BWr9dg0iPG90G5yfVQ+p/rGNMXdbfStvzQZEVEi4A==} @@ -21708,6 +21775,21 @@ packages: dependencies: kind-of: 6.0.3 + /sharp@0.32.6: + resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} + engines: {node: '>=14.15.0'} + requiresBuild: true + dependencies: + color: 4.2.3 + detect-libc: 2.0.2 + node-addon-api: 6.1.0 + prebuild-install: 7.1.1 + semver: 7.5.4 + simple-get: 4.0.1 + tar-fs: 3.0.4 + tunnel-agent: 0.6.0 + dev: true + /shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -21778,6 +21860,18 @@ packages: - supports-color dev: true + /simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: true + + /simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + dev: true + /simple-plist@1.3.1: resolution: {integrity: sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==} dependencies: @@ -21790,7 +21884,6 @@ packages: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} dependencies: is-arrayish: 0.3.2 - dev: false /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -22207,6 +22300,13 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + /streamx@2.15.2: + resolution: {integrity: sha512-b62pAV/aeMjUoRN2C/9F0n+G8AfcJjNC0zw/ZmOHeFsIe4m4GzjVW9m6VHXVjk536NbdU9JRwKMJRfkc+zUFTg==} + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + dev: true + /string-hash@1.1.3: resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} dev: true @@ -22372,7 +22472,6 @@ packages: /strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} - dev: false /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} @@ -22631,6 +22730,14 @@ packages: tar-stream: 2.2.0 dev: true + /tar-fs@3.0.4: + resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==} + dependencies: + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 3.1.6 + dev: true + /tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} @@ -22642,6 +22749,14 @@ packages: readable-stream: 3.6.2 dev: true + /tar-stream@3.1.6: + resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==} + dependencies: + b4a: 1.6.4 + fast-fifo: 1.3.2 + streamx: 2.15.2 + dev: true + /tar@6.1.11: resolution: {integrity: sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==} engines: {node: '>= 10'} @@ -23145,6 +23260,12 @@ packages: - supports-color dev: true + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: true + /turbo-darwin-64@1.9.3: resolution: {integrity: sha512-0dFc2cWXl82kRE4Z+QqPHhbEFEpUZho1msHXHWbz5+PqLxn8FY0lEVOHkq5tgKNNEd5KnGyj33gC/bHhpZOk5g==} cpu: [x64]