diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index cb4ed314f..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 RC)](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/.github/workflows/issue-commenter.yml b/.github/workflows/issue-commenter.yml index 2f7bb8c95..8a413dac7 100644 --- a/.github/workflows/issue-commenter.yml +++ b/.github/workflows/issue-commenter.yml @@ -19,9 +19,8 @@ jobs: Unfortunately, the reproduction is missing or incomplete, and as such we cannot investigate this issue. Please add a reproduction to the issue, otherwise it will be closed automatically. **Templates:** - - [CodeSandbox (`app` directory)](https://codesandbox.io/p/sandbox/next-intl-bug-template-forked-yow8ep) - - [CodeSandbox (`app` directory, RSC RC)](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) **Creating a good bug report takes time.** diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 279ab22e3..8a007427f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,6 +20,12 @@ jobs: cache: 'pnpm' - run: pnpm install + # Next.js caching + - uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/examples/*/.next/cache + key: ${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml') }} + # Playwright - run: | PLAYWRIGHT_VERSION=$(cat pnpm-lock.yaml | grep /@playwright/test@ | sed 's/.*@\([^:]*\):.*/\1/') @@ -32,7 +38,7 @@ jobs: key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }} - if: steps.cache-playwright-browsers.outputs.cache-hit != 'true' # Setting up playwright in one example is sufficient - run: pnpm --filter example-next-13 exec playwright install --with-deps + run: pnpm --filter example-next-13-advanced exec playwright install --with-deps - run: pnpm run build - run: pnpm run lint diff --git a/README.md b/README.md index fe5dda024..c9086ca9b 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ export default function UserProfile({user}) { } ``` -```json +```js // en.json { "UserProfile": { diff --git a/docs/components/CodeSnippets.tsx b/docs/components/CodeSnippets.tsx index 6193863de..5bb02eb52 100644 --- a/docs/components/CodeSnippets.tsx +++ b/docs/components/CodeSnippets.tsx @@ -19,7 +19,7 @@ function icu() { : - "{'{'}username{'}'}'s profile" + "{'{'}firstName{'}'}'s profile" , diff --git a/docs/components/Hero.tsx b/docs/components/Hero.tsx index c99fceb37..81773972f 100644 --- a/docs/components/Hero.tsx +++ b/docs/components/Hero.tsx @@ -47,7 +47,7 @@ export default function Hero({ 📣{' '} {rscAnnouncement} diff --git a/docs/components/HeroCode.tsx b/docs/components/HeroCode.tsx index 313749516..34291eea9 100644 --- a/docs/components/HeroCode.tsx +++ b/docs/components/HeroCode.tsx @@ -195,7 +195,7 @@ const files = [ : - "{'{'}username{'}'}'s profile" + "{'{'}firstname{'}'}'s profile" , 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..4e21cad8b 100644 --- a/docs/next.config.js +++ b/docs/next.config.js @@ -28,14 +28,24 @@ module.exports = withNextra({ destination: '/docs/getting-started', permanent: true }, + { + source: '/docs/configuration', + destination: '/docs/usage/configuration', + permanent: true + }, { 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 +55,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 +95,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/package.json b/docs/package.json index bca594e51..0d4ae83f3 100644 --- a/docs/package.json +++ b/docs/package.json @@ -24,14 +24,14 @@ "tailwindcss": "^3.3.2" }, "devDependencies": { - "@types/node": "20.1.2", - "@types/react": "^18.2.5", + "@types/node": "^20.1.2", + "@types/react": "^18.2.29", "autoprefixer": "^10.4.0", "eslint": "^8.46.0", "eslint-config-molindo": "^7.0.0", "eslint-config-next": "^13.4.0", "next-sitemap": "^4.0.7", - "typescript": "^5.0.0" + "typescript": "^5.2.2" }, "funding": "https://github.com/amannn/next-intl?sponsor=1" } diff --git a/docs/pages/blog/next-intl-3-0.mdx b/docs/pages/blog/next-intl-3-0.mdx index 97848c7b8..e7a36ce7b 100644 --- a/docs/pages/blog/next-intl-3-0.mdx +++ b/docs/pages/blog/next-intl-3-0.mdx @@ -1,36 +1,38 @@ --- -title: next-intl 3.0 release candidate +title: next-intl 3.0 --- -# next-intl 3.0 release candidate +import PartnerContentLink from 'components/PartnerContentLink'; -Sep 28, 2023 · by Jan Amann +# next-intl 3.0 -Almost one year ago, on Oct 25, 2022, [Next.js 13 was announced](https://nextjs.org/blog/next-13) with beta support for the App Router and Server Components. Ever since then, `next-intl` began an exploration on what it means to provide an ideal experience for implementing i18n with the newly added capabilities. +Nov 14, 2023 · by Jan Amann -Today, [after more than 300 commits and the involvement of over 50 community members](https://github.com/amannn/next-intl/pull/149), I'm absolutely thrilled to share **the first release candidate of `next-intl` 3.0**, which is now App Router-first. +More than a year ago, on Oct 25, 2022, [Next.js 13 was announced](https://nextjs.org/blog/next-13) with beta support for the App Router and Server Components. Ever since then, `next-intl` began an exploration on what it means to provide an ideal experience for implementing i18n with the newly added capabilities. -If you're still happy with the Pages Router, rest assured that `next-intl` is dedicated to support this paradigm for as long as Next.js does. For those who have already migrated to the App Router, this means that `next-intl` now takes full advantage of the new capabilities. +Today, [after more than 370 commits and the involvement of over 60 community members](https://github.com/amannn/next-intl/pull/149), I'm absolutely thrilled to announce **next-intl 3.0**, which is now App Router-first. + +If you're still happy with the Pages Router, rest assured that `next-intl` is dedicated to support this paradigm for as long as Next.js does. For those who have already migrated to the App Router, this means that you can take full advantage of the new capabilities with `next-intl`. ## 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 `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)). +1. **Support for React Server Components**: The APIs `useTranslations`, `useFormatter`, `useLocale`, `useNow` and `useTimeZone` can now be used in Server Components ([docs](/docs/environments/server-client-components)). +2. **New async APIs**: To handle i18n in async components, the Metadata API and Route Handlers, the APIs `getTranslations`, `getFormatter`, `getNow`, and `getTimeZone` have been added ([docs](/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 ([docs](/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 [docs](/docs/routing/navigation)). The latter two have already been added in minor versions, but 3.0 cleans up the API and includes many improvements and bug fixes. ## Breaking changes -If you've been part of the Server Components beta and have already tried out previous releases, first of all, thank you so much! Second: Some APIs saw iterations over the beta period, please carefully review the breaking changes below, even if you're already using some of the new APIs. +If you've already tried out pre-release versions, first of all, thank you so much! Second: Some APIs saw iterations over the beta period, please carefully review the breaking changes below, even if you're already using some of the new APIs. ### Updated setup `next-intl` now requires two additional setup steps when you're using the App Router: -1. [The `i18n.ts` module](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/getting-started/app-router-server-components#i18nts) provides configuration for Server Components -2. [`next-intl/plugin`](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/getting-started/app-router-server-components#nextconfigjs) needs to be added to link your `i18n.ts` module to `next-intl` +1. [The `i18n.ts` module](/docs/getting-started/app-router#i18nts) provides configuration for Server Components +2. [`next-intl/plugin`](/docs/getting-started/app-router#nextconfigjs) needs to be added to link your `i18n.ts` module to `next-intl` ### New navigation APIs for the App Router @@ -49,9 +51,9 @@ With 3.0, we're cleaning up these APIs by moving them to a shared namespace as w + const {Link, useRouter, usePathname, redirect} = createSharedPathnamesNavigation({locales}); ``` -Typically, you'll want to call this factory function in a central place in your app where you can easily import from (see [the proposed navigation docs](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/routing/navigation#shared-pathnames)). +Typically, you'll want to call this factory function in a central place in your app where you can easily import from (see [the navigation docs](/docs/routing/navigation#shared-pathnames)). -These changes bring the existing APIs in line with the new [`createLocalizedPathnamesNavigation` API](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/routing/navigation#localized-pathnames) that allows you to localize pathnames: +These changes bring the existing APIs in line with the new [`createLocalizedPathnamesNavigation` API](/docs/routing/navigation#localized-pathnames) that allows you to localize pathnames: ```tsx filename="navigation.ts" import {createLocalizedPathnamesNavigation, Pathnames} from 'next-intl/navigation'; @@ -82,44 +84,61 @@ By using a similar API, you can upgrade from shared pathnames to localized pathn ### Switching the middleware default of `localePrefix` to `always` -Previously, the [`localePrefix` of the middleware](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/routing/middleware#locale-prefix) defaulted to `as-needed`, meaning that a locale prefix was only added for non-default locales. +Previously, the [`localePrefix` of the middleware](/docs/routing/middleware#locale-prefix) defaulted to `as-needed`, meaning that a locale prefix was only added for non-default locales. This default has now been changed to `always` since this has two advantages: -1. We can recommend [a safer default `matcher`](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/routing/middleware#matcher-config) that needs no extra treatment for pathnames with dots (e.g. `/users/jane.doe`) -2. It avoids an [edge case of `Link`](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/routing/middleware#locale-prefix-as-needed) where we include a prefix for the default locale on the server side but patch this on the client side by removing the prefix (certain SEO tools might report a hint that a link points to a redirect in this case). +1. We can recommend [a safer default `matcher`](/docs/routing/middleware#matcher-config) that needs no extra treatment for pathnames with dots (e.g. `/users/jane.doe`) +2. It avoids an [edge case of `Link`](/docs/routing/middleware#locale-prefix-as-needed) where we include a prefix for the default locale on the server side but patch this on the client side by removing the prefix (certain SEO tools might report a hint that a link points to a redirect in this case). -If you want to stay on the `as-needed` strategy, you can [configure this option](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/routing/middleware#locale-prefix-as-needed) in the middleware. +If you want to stay on the `as-needed` strategy, you can [configure this option](/docs/routing/middleware#locale-prefix-as-needed) in the middleware. ### Static rendering of Server Components With the newly introduced Server Components support comes a temporary workaround for static rendering. -If you call APIs like `useTranslations` in a Server Component, by default, the hook will opt the page into dynamic rendering. This is a limitation that we aim to remove in the future, but as a stopgap solution, we've added the [`unstable_setRequestLocale`](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/getting-started/app-router-server-components#static-rendering) API so that you can keep your pages fully static. +If you call APIs from `next-intl` in Server Components, the page will by default opt into [dynamic rendering](https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-rendering). This is a limitation that we aim to remove in the future, but as a stopgap solution, we've added the [`unstable_setRequestLocale`](/docs/getting-started/app-router-server-components#static-rendering) API so that you can keep your pages fully static. Note that if you're using `next-intl` exclusively in Client Components, this doesn't apply to your app and static rendering will continue to work as it did before. ### Other notable changes 1. `next-intl` now uses [`exports` in `package.json`](https://nodejs.org/api/packages.html#subpath-exports) to clearly define which modules are exported. This should not affect you, unless you've previously imported undocumented internals. -2. `NextIntlProvider` has been removed in favor of [`NextIntlClientProvider`](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/configuration#client-server-components) +2. `NextIntlProvider` has been removed in favor of [`NextIntlClientProvider`](/docs/usage/configuration#client-server-components) 3. `NextIntlClientProvider` now needs to be imported from `next-intl` instead of `next-intl/client`. -4. [The middleware](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/routing/middleware) now needs to be imported from `next-intl/middleware` instead of `next-intl/server` (deprecated since v2.14). +4. [The middleware](/docs/routing/middleware) now needs to be imported from `next-intl/middleware` instead of `next-intl/server` (deprecated since v2.14). 5. `next@^13.4` is now required for the RSC APIs. Next.js 12 is still supported for the Pages Router integration. 6. If you're using `NextIntlClientProvider` outside of the App Router (e.g. with the Pages Router), you need to define the `locale` prop explicitly. 7. `useMessages` now has a non-nullable return type for easier consumption and will throw if no messages are configured. -8. `createTranslator(…).rich` now returns a `ReactNode`. Previously, this was somewhat confusing, since `t.rich` accepted and returned either React elements or strings depending on if you retrieve the fuction via `useTranslations` or `createTranslator`. Now, an explicit [`t.markup`](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/usage/messages#html-markup) function has been added to generate markup strings like `'Hello'` outside of React components. -9. `useIntl` has been replaced with [`useFormatter`](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/usage/dates-times) (deprecated since v2.11). -10. `createIntl` has been replaced with [`createFormatter`](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/docs/environments/core-library) (deprecated since v2.11). +8. `createTranslator(…).rich` now returns a `ReactNode`. Previously, this was somewhat confusing, since `t.rich` accepted and returned either React elements or strings depending on if you retrieve the fuction via `useTranslations` or `createTranslator`. Now, an explicit [`t.markup`](/docs/usage/messages#html-markup) function has been added to generate markup strings like `'Hello'` outside of React components. +9. `useIntl` has been replaced with [`useFormatter`](/docs/usage/dates-times) (deprecated since v2.11). +10. `createIntl` has been replaced with [`createFormatter`](/docs/environments/core-library) (deprecated since v2.11). ## Upgrade now -Along with the release candidate also comes a [preview of the updated docs](https://next-intl-docs-git-feat-next-13-rsc-next-intl.vercel.app/). - -We're still looking for more feedback, please try out the release candidate and [reach out](https://github.com/amannn/next-intl/pull/149)! - ``` -npm install next-intl@3.0.0-rc.10 +npm install next-intl@3.0.0 ``` -This release was truly a team effort and couldn't be nearly where it is today without the involvement of the community. Thank you so much for being part of this! +## Thank you + +This release was truly a team effort and couldn't be nearly where it is today without the involvement of the community. + +Thank you so much for: + +- All the encouraging words along the way +- Testing out prereleases +- Providing feedback +- Contributing code +- Questioning ideas +- Helping each other + +I had the pleasure to get in touch with so many of you along the way and I'm incredibly grateful for the willingness to help and support each other in our community. + +A special thank you goes to Crowdin, being the primary sponsor for `next-intl` and enabling me to regularly dedicate time for this project. + +—Jan + +--- + +(this post has been updated from an initial announcement for the 3.0 release candidate) diff --git a/docs/pages/blog/translations-outside-of-react-components.mdx b/docs/pages/blog/translations-outside-of-react-components.mdx index fb4077ec1..3280699c8 100644 --- a/docs/pages/blog/translations-outside-of-react-components.mdx +++ b/docs/pages/blog/translations-outside-of-react-components.mdx @@ -113,26 +113,21 @@ Additionally, this approach is more robust to possibly unexpected states, like t If you’re working with Next.js, you might want to translate i18n messages in [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes), [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) or the [Metadata API](https://nextjs.org/docs/app/api-reference/file-conventions/metadata). -`next-intl` provides a [core library](/docs/environments/core-library) that is agnostic from React and can be used for these cases. +`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 {createTranslator} from 'next-intl'; +import {getTranslations} from 'next-intl/server'; -const messages = { - hello: 'Hello {name}!' -}; +// The `locale` is received from Next.js via `params` +const locale = params.locale; // This creates the same function that is returned by `useTranslations`. -// Since there's no provider, you can pass all the properties you'd -// usually pass to the provider directly here. -const t = createTranslator({locale: 'en', messages}); +const t = await getTranslations({locale}); // Result: "Hello world!" t('hello', {name: 'world'}); ``` -There's currently a proposal to further simplify this use case, by offering a set of [new APIs that integrate with Server Components](/docs/environments/metadata-route-handlers) (currently in beta). - ## This seems familiar If you’ve been working with React for a longer time, you might have experienced the change [from `component{DidMount,DidUpdate,WillUnmount}` to `useEffect`](https://legacy.reactjs.org/docs/hooks-effect.html#explanation-why-effects-run-on-each-update). The reason why `useEffect` is superior is because it nudges the developer into a direction where the app is always in sync and by doing this, a whole array of potential issues just magically disappear. diff --git a/docs/pages/docs/_meta.json b/docs/pages/docs/_meta.json index bb4c20f62..097f7bf7f 100644 --- a/docs/pages/docs/_meta.json +++ b/docs/pages/docs/_meta.json @@ -2,9 +2,7 @@ "getting-started": "Getting started", "usage": "Usage guide", "environments": "Environments", - "configuration": "Global configuration", "routing": "Routing", "workflows": "Workflows & integrations", - "production-checklist": "Production checklist", "faq": "FAQ" } \ No newline at end of file diff --git a/docs/pages/docs/environments/_meta.json b/docs/pages/docs/environments/_meta.json index 62dc74241..d5ee47831 100644 --- a/docs/pages/docs/environments/_meta.json +++ b/docs/pages/docs/environments/_meta.json @@ -3,5 +3,6 @@ "server-client-components": "Server & Client Components", "metadata-route-handlers": "Metadata & Route Handlers", "error-files": "Error files (e.g. not-found)", - "core-library": "Core library" + "core-library": "Core library", + "runtime-requirements": "Runtime requirements" } diff --git a/docs/pages/docs/environments/core-library.mdx b/docs/pages/docs/environments/core-library.mdx index c9fc2bf9f..b8c6fc2e3 100644 --- a/docs/pages/docs/environments/core-library.mdx +++ b/docs/pages/docs/environments/core-library.mdx @@ -18,15 +18,15 @@ const t = createTranslator({locale: 'en', messages}); // Result: "Hello world!" t('basic', {name: 'world'}); -// Rich text uses functions that accept and return a string. -// Result: "Hello world!" -t.rich('rich', { +// To generate HTML markup, you can consider using the `markup` +// function which in contrast to `t.rich` returns a markup string. +t.markup('rich', { name: 'world', b: (chunks) => `${chunks}` }); ``` -For date, time and number formatting, the `intl` object can be created outside of React as well: +For date, time and number formatting, the `format` object can be created outside of React as well: ```js import {createFormatter} from 'next-intl'; diff --git a/docs/pages/docs/environments/error-files.mdx b/docs/pages/docs/environments/error-files.mdx index fc095a991..52a0d2828 100644 --- a/docs/pages/docs/environments/error-files.mdx +++ b/docs/pages/docs/environments/error-files.mdx @@ -1,4 +1,3 @@ -import VersionTabs from 'components/VersionTabs'; import Callout from 'components/Callout'; # Internationalization in Next.js error files @@ -42,7 +41,7 @@ export default function CatchAllPage() { } ``` -After this change, all requests that are matched within the `[locale]` segment will render the `not-found` page when an unknown route is encountered. +After this change, all requests that are matched within the `[locale]` segment will render the `not-found` page when an unknown route is encountered (e.g. `/en/unknown`). ### Catching non-localized requests @@ -53,24 +52,20 @@ You can add a root `not-found` page to handle these cases too. ```tsx filename="app/not-found.tsx" 'use client'; -import Error from 'next/error'; +import {redirect, usePathname} from 'next/navigation'; -// Render the default Next.js 404 page when a route -// is requested that doesn't match the middleware and -// therefore doesn't have a locale associated with it. +// Can be imported from a shared config +const defaultLocale = 'en'; export default function NotFound() { - return ( - - - - - - ); + const pathname = usePathname(); + + // Add a locale prefix to show a localized not found page + redirect(`/${defaultLocale}${pathname}`); } ``` -Note that the presence of `app/not-found.tsx` requires that a root layout is available as well, even if it's just passing `children` through. +Note that the presence of `app/not-found.tsx` requires that a root layout is available, even if it's just passing `children` through. ```tsx filename="app/layout.tsx" // Since we have a root `not-found.tsx` page, a layout file @@ -89,12 +84,8 @@ import {notFound} from 'next/navigation'; const locales = ['en', 'de']; export default function LocaleLayout({children, params}) { - const locale = useLocale(); - - // Validate that the incoming `locale` parameter is a valid locale - if (params.locale !== locale) { - notFound(); - } + // Validate that the incoming `locale` parameter is valid + if (!locales.includes(locale as any)) notFound(); return ( @@ -125,11 +116,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 +140,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 +158,14 @@ export default function Error({error, reset}) { ); } ``` + +Note that `error.tsx` is loaded as soon as the app starts. If your app is performance-senstive and you want to avoid loading translation functionality from `next-intl` as part of the initial bundle, you can export a lazy reference from your `error` file: + +```tsx filename="app/[locale]/error.tsx" +'use client'; + +import {lazy} from 'react'; + +// Move error content to a separate chunk and load it only when needed +export default lazy(() => import('./Error')); +``` 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 857080547..cc18a4c52 100644 --- a/docs/pages/docs/environments/metadata-route-handlers.mdx +++ b/docs/pages/docs/environments/metadata-route-handlers.mdx @@ -1,8 +1,7 @@ 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 +# Internationalization of Metadata & Route Handlers with the Next.js App Router There are a few places in Next.js apps where you might need to apply internationalization outside of React components: @@ -10,87 +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) -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. - -```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 [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. - - - - +`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 {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'; +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') @@ -98,74 +27,40 @@ export async function generateMetadata({params: {locale}}) { } ``` - - + + By passing an explicit `locale` to the awaitable functions from `next-intl`, + you can make the metadata handler eligable for [static + rendering](/docs/getting-started/app-router#static-rendering). + ### 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/og'; -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/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 -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 `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" +```tsx filename="app/api/hello/route.tsx" import {NextResponse} from 'next/server'; -import {createTranslator} from 'next-intl'; +import {getTranslations} from 'next-intl/server'; -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')}); -} -``` - - - +export async function GET(request) { + // Example: Receive the `locale` via a search param + const {searchParams} = new URL(request.url); + const locale = searchParams.get('locale'); -```tsx filename="app/[locale]/hello/route.tsx" -import {NextResponse} from 'next/server'; -import {getTranslator} from 'next-intl/server'; - -export async function GET(request, {params: {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/production-checklist.mdx b/docs/pages/docs/environments/runtime-requirements.mdx similarity index 60% rename from docs/pages/docs/production-checklist.mdx rename to docs/pages/docs/environments/runtime-requirements.mdx index b01c483c9..5930e873d 100644 --- a/docs/pages/docs/production-checklist.mdx +++ b/docs/pages/docs/environments/runtime-requirements.mdx @@ -1,20 +1,12 @@ 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: +## Browser -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#formats) which ensure consistent formatting across the app. -5. Please check the [runtime requirements](#runtime-requirements) and optionally provide polyfills. +The source code of `next-intl` is compiled for [the same browsers that Next.js supports](https://nextjs.org/docs/architecture/supported-browsers). -## Runtime requirements - -### Browser - -Based on the features you're using, you have to make sure your browser supports the following APIs: +Based on the features you're using, you have to make sure your target browsers support the following APIs: - Basic usage: `Intl.Locale` ([compatibility](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#browser_compatibility)) - Date & time formatting: `Intl.DateTimeFormat` ([compatibility](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#browser_compatibility)) @@ -31,7 +23,6 @@ If you target a browser that doesn't support all the required APIs, consider usi import {useLocale} from 'next-intl'; import Script from 'next/script'; -// Use this component in your Next.js custom `_app` function IntlPolyfills() { const locale = useLocale(); @@ -68,6 +59,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. +The minimum version to support all relevant `Intl` APIs 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 4ab06f28a..c94e1caa5 100644 --- a/docs/pages/docs/environments/server-client-components.mdx +++ b/docs/pages/docs/environments/server-client-components.mdx @@ -1,14 +1,8 @@ import Callout from 'components/Callout'; -# Internationalization of Server & Client Components in Next.js 13 +# Internationalization of Server & Client Components - - -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. +[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. @@ -16,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'); @@ -24,28 +18,106 @@ export default function Index() { } ``` -## Benefits of handling i18n in Server Components [#server-components-benefits] - Moving internationalization to the server side unlocks new levels of performance, leaving the client side for interactive features. - -
    -
  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. -
-
+**Benefits of server-side internationalization:** + +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 + +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. + +```tsx filename="[locale]/profile/page.tsx" +import {getTranslations} from 'next-intl/server'; + +export default async function ProfilePage() { + const user = await fetchUser(); + const t = await getTranslations('ProfilePage'); + + 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(); +const locale = await getLocale(); +``` + +### 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. + +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})}

+
+ ); +} +``` + +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). + +
+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. + +
+ +
+Should I use async or non-async functions for my components? + +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. + +
## Using internationalization in Client Components @@ -60,7 +132,10 @@ import {useTranslations} from 'next-intl'; import Expandable from './Expandable'; export default function FAQEntry() { + // Call `useTranslations` in a Server Component ... const t = useTranslations('FAQEntry'); + + // ... and pass translated content to a Client Component return ( @@ -92,7 +167,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 @@ -127,18 +202,15 @@ If you need to incorporate dynamic state that can not be moved to the server sid ```tsx filename="Counter.tsx" import pick from 'lodash/pick'; -import {useLocale, NextIntlClientProvider} from 'next-intl'; +import {NextIntlClientProvider} from 'next-intl'; import ClientCounter from './ClientCounter'; export default function Counter() { - const locale = useLocale(); - // Receive messages provided in `i18n.ts` const messages = useMessages(); return ( - `NextIntlClientProvider` doesn't automatically inherit configuration from - `i18n.ts`, therefore make sure to provide all relevant props on the component. - If you're configuring non-serializable values like functions, you have to mark - the component that renders `NextIntlClientProvider` with `'use client'` - ([example](https://codesandbox.io/p/sandbox/next-intl-non-serializable-props-on-nextintlclientprovider-r0h2hi?file=%2Fsrc%2Fapp%2F%5Blocale%5D%2FNextIntlProvider.tsx)). + `NextIntlClientProvider` inherits the props `locale`, `now` and `timeZone` + when the component is rendered from a Server Component. Other configuration + properties like `messages` and `formats` can be provided as necessary. ### Option 4: Providing all messages @@ -169,6 +239,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(); @@ -185,13 +257,13 @@ 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 -### "Failed to call `useTranslations`, because the context from `NextIntlClientProvider` was not found." [#missing-context] +### "Failed to call `useTranslations` because the context from `NextIntlClientProvider` was not found." [#missing-context] You might encounter this error or a similar one referencing `useFormatter` while working on your app. @@ -199,3 +271,48 @@ This can happen because: 1. The component that calls the hook accidentally ended up in a client-side module graph, but you expected it to render as a Server Component. If this is the case, try to [pass this component via `children`](#option-1-passing-translations-to-client-components) to the Client Component instead. 2. You're intentionally calling the hook from a Client Component, but `NextIntlClientProvider` is not present as an ancestor in the component tree. If this is the case, you can [wrap your component in `NextIntlClientProvider`](#option-3-providing-individual-messages) to resolve this error. + +### "Functions cannot be passed directly to Client Components because they're not serializable." [#non-serializable-props] + +You might encounter this error when you try to pass a non-serializable prop to `NextIntlClientProvider`. + +The component accepts the following props that are not serializable: + +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: + +```tsx filename="MyCustomNextIntlClientProvider.tsx" +'use client'; + +import {NextIntlClientProvider} from 'next-intl'; + +export default function MyCustomNextIntlClientProvider({ + locale, + timeZone, + now, + ...rest +}) { + return ( + {text} + }} + // Make sure to forward these props to avoid markup mismatches + locale={locale} + timeZone={timeZone} + now={now} + {...props} + /> + ); +} +``` + +([working example](https://codesandbox.io/p/sandbox/next-intl-non-serializable-props-on-nextintlclientprovider-r0h2hi?file=%2Fsrc%2Fapp%2F%5Blocale%5D%2FNextIntlProvider.tsx)) + +By doing this, your custom provider will already be part of the client-side bundle and can therefore define and pass functions as props. + +**Important:** Be sure to pass explicit `locale`, `timeZone` and `now` props to `NextIntlClientProvider` in this case, since the props aren't automatically inherited from a Server Component when you import `NextIntlClientProvider` from a Client Component. 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:** - -
    -
  • - 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)). -
  • -
  • 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). -
  • -
- -
diff --git a/docs/pages/docs/getting-started/app-router-server-components.mdx b/docs/pages/docs/getting-started/app-router-server-components.mdx deleted file mode 100644 index 36b704aba..000000000 --- a/docs/pages/docs/getting-started/app-router-server-components.mdx +++ /dev/null @@ -1,260 +0,0 @@ -import Callout from 'components/Callout'; -import Steps from 'components/Steps'; - -# Next.js 13: Internationalization (i18n) in Server Components - -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 preview version to early adopters, who are already building apps with Server Components. - - - This page covers a getting started guide for the React Server Components beta that was running over the last couple of months. It's recommended to try out the [next-intl 3.0 release candidate](/blog/next-intl-3-0) if you'd like to use `next-intl` in Server Components. - - -## Current version - -``` -npm install next-intl@3.0.0-beta.19 -``` - -This 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)). - - -## Getting started - -If you haven't done so already, [create a Next.js 13 app that uses the App Router](https://nextjs.org/docs/getting-started/installation). All pages should be moved within a `[locale]` folder so that we can use this segment to provide content in different languages (e.g. `/en`, `/en/about`, etc.). - -**Start by running `npm install next-intl` and create the following file structure:** - -``` -├── messages (1) -│ ├── en.json -│ └── ... -├── i18n.ts (2) -├── next.config.js (3) -├── middleware.ts (4) -└── app - └── [locale] - ├── layout.tsx (5) - └── page.tsx (6) -``` - -**Now, set up the 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!" - } -} -``` - -### `i18n.ts` - -`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. - -```tsx filename="i18n.ts" -import {getRequestConfig} from 'next-intl/server'; - -export default getRequestConfig(async ({locale}) => ({ - messages: (await import(`./messages/${locale}.json`)).default -})); -``` - -### `next.config.js` - -Now, set up the plugin and provide the path to your `i18n.ts` file. - -```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' -); - -module.exports = withNextIntl({ - // Other Next.js configuration ... -}); -``` - -### `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` - -The `locale` that was matched by the middleware is available via the `locale` param and can be used to configure the document language. - -```tsx filename="app/[locale]/layout.tsx" -import {useLocale} from 'next-intl'; -import {notFound} from 'next/navigation'; - -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(); - - return ( - - {children} - - ); -} -``` - -### `app/[locale]/page.tsx` - -Use translations in your page components or anywhere else! - -```tsx filename="app/[locale]/page.tsx" -import {useTranslations} from 'next-intl'; - -export default function Index() { - const t = useTranslations('Index'); - return

{t('title')}

; -} -``` - -
- -That's all it takes! Now you can internationalize your apps on the server side. - - - -**Next steps:** - -
    -
  • - Ran into an issue? Have a look at [the Server Components - example](https://next-intl-example-next-13-git-feat-next-13-rsc-amannn.vercel.app/) - ([source](https://github.com/amannn/next-intl/tree/feat/next-13-rsc/examples/example-next-13)). -
  • -
  • 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). -
  • -
  • - Interested to learn more about the advantages of using `next-intl` in Server - Components? Check out the [Server & Client Components - guide](/docs/environments/server-client-components). -
  • -
  • - Are you transitioning from the `pages` directory to `app`? Check out the - [migration - example](https://github.com/amannn/next-intl/tree/feat/next-13-rsc/examples/example-next-13-with-pages). -
  • -
- -
- -## Static rendering - -By using APIs like `useTranslations` from `next-intl` in Server Components, your pages will currently opt into dynamic rendering. This is a limitation that we aim to remove in the future, but as a stopgap solution, `next-intl` provides a temporary API that can be used to enable static rendering: - - - -### Add `generateStaticParams` to `app/[locale]/layout.tsx` - -Since we use a dynamic route segment for the `[locale]` param, we need to provide all possible values via [`generateStaticParams`](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) to Next.js, so the routes can be rendered at build time. - -```tsx filename="app/[locale]/layout.tsx" -const locales = ['en', 'de']; - -export function generateStaticParams() { - return locales.map((locale) => ({locale})); -} -``` - -### Add `unstable_setRequestLocale` to all layouts and pages - -`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'; - -const locales = ['en', 'de']; - -export default async function LocaleLayout({ - children, - params: {locale} -}) { - // Validate that the incoming `locale` parameter is valid - const isValidLocale = locales.some((cur) => cur === locale); - if (!isValidLocale) notFound(); - - unstable_setRequestLocale(locale); - - return ( - // ... - ); -} -``` - -```tsx filename="app/[locale]/page.tsx" -import {unstable_setRequestLocale} from 'next-intl/server'; -import {locales} from '..'; - -export default async function IndexPage({ - params: {locale} -}) { - unstable_setRequestLocale(locale); - - return ( - // ... - ); -} -``` - -**What does "unstable" mean?** - -`unstable_setRequestLocale` is meant to be used as a stopgap solution and we aim to remove it in the future [in case Next.js adds an API to access parts of the URL](https://github.com/facebook/react/pull/27424#issuecomment-1739464985). If that's the case, 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 `privacy/page.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/app-router.mdx b/docs/pages/docs/getting-started/app-router.mdx new file mode 100644 index 000000000..00d531226 --- /dev/null +++ b/docs/pages/docs/getting-started/app-router.mdx @@ -0,0 +1,295 @@ +import Callout from 'components/Callout'; +import Steps from 'components/Steps'; + +# Next.js App Router Internationalization (i18n) + +The Next.js App Router introduces support for [React Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components) and unlocks [many benefits](/docs/environments/server-client-components) when handling internationalization on the server side. + +## Getting started + +If you haven't done so already, [create a Next.js app that uses the App Router](https://nextjs.org/docs/getting-started/installation). + +`next-intl` integrates with the App Router, by using a `[locale]` [dynamic segment](https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes) so that we can use this segment to provide content in different languages (e.g. `/en`, `/en/about`, etc.). + +Let's get started! + +**Run `npm install next-intl` and create the following file structure:** + +``` +├── messages (1) +│ ├── en.json +│ └── ... +├── 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:** + + + +### `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 add JSON files in your project based on locales, e.g. `en.json`. + +```json filename="messages/en.json" +{ + "Index": { + "title": "Hello world!" + } +} +``` + +### `next.config.js` + +Now, set up the plugin which creates an alias to import your i18n configuration (specified in the next step) into 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. Here you can provide messages and other options depending on the locale of the user. + +```tsx filename="src/i18n.ts" +import {getRequestConfig} from 'next-intl/server'; + +export default getRequestConfig(async ({locale}) => ({ + messages: (await import(`../messages/${locale}.json`)).default +})); +``` + +
+ Can I move this file somewhere else? + +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')( + // Specify a custom path here + './somewhere/else/i18n.ts' +); + +module.exports = withNextIntl({ + // Other Next.js configuration ... +}); +``` + +
+ +### `middleware.ts` + +The 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'], + + // Used when no locale matches + defaultLocale: 'en' +}); + +export const config = { + // Match only internationalized pathnames + matcher: ['/', '/(de|en)/:path*'] +}; +``` + +### `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. + +```tsx filename="app/[locale]/layout.tsx" +import {notFound} from 'next/navigation'; + +// Can be imported from a shared config +const locales = ['en', 'de']; + +export default function LocaleLayout({children, params: {locale}}) { + // Validate that the incoming `locale` parameter is valid + if (!locales.includes(locale as any)) notFound(); + + return ( + + {children} + + ); +} +``` + +### `app/[locale]/page.tsx` + +Use translations in your page components or anywhere else! + +```tsx filename="app/[locale]/page.tsx" +import {useTranslations} from 'next-intl'; + +export default function Index() { + const t = useTranslations('Index'); + return

{t('title')}

; +} +``` + +
+ +That's all it takes! + + + +**Next steps:** + +
    +
  • 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/). +
  • +
  • + 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). +
  • +
  • + Are you transitioning from the Pages to the App Router? Check out [the + migration example](/examples/app-router-migration). +
  • +
+ +
+ +## Static rendering + +By using APIs like `useTranslations` from `next-intl` in Server Components, your pages will currently opt into [dynamic rendering](https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-rendering). This is a limitation that we aim to remove in the future, but as a stopgap solution, `next-intl` provides a temporary API that can be used to enable static rendering: + + + +### Add `generateStaticParams` to `app/[locale]/layout.tsx` + +Since we use a dynamic route segment for the `[locale]` param, we need to provide all possible values via [`generateStaticParams`](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) to Next.js, so the routes can be rendered at build time. + +```tsx filename="app/[locale]/layout.tsx" +const locales = ['en', 'de']; + +export function generateStaticParams() { + return locales.map((locale) => ({locale})); +} +``` + +### Add `unstable_setRequestLocale` to all layouts and pages + +`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'; + +const locales = ['en', 'de']; + +export default async function LocaleLayout({ + children, + params: {locale} +}) { + // Validate that the incoming `locale` parameter is valid + if (!locales.includes(locale as any)) notFound(); + + unstable_setRequestLocale(locale); + + return ( + // ... + ); +} +``` + +```tsx filename="app/[locale]/page.tsx" +import {unstable_setRequestLocale} from 'next-intl/server'; +import {locales} from '..'; + +export default function IndexPage({ + params: {locale} +}) { + unstable_setRequestLocale(locale); + + // Once the request locale is set, you + // can call hooks from `next-intl` + const t = useTranslations('IndexPage'); + + return ( + // ... + ); +} +``` + +**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. + +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`. + +
+What does "unstable" mean? + +`unstable_setRequestLocale` is meant to be used as a stopgap solution and we aim to remove it in the future [in case Next.js adds an API to access parts of the URL](https://github.com/facebook/react/pull/27424#issuecomment-1739464985). If that's the case, you'll get a deprecation notice in a minor version and the API will be removed as part of a major version. + +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? + +`next-intl` uses [`cache()`](https://react.dev/reference/react/cache) to create a mutable store that holds the current locale. By calling `unstable_setRequestLocale`, the current locale will be written to the store, making it available to all APIs that require the locale. + +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. + +
+ +
+Why is this API necessary? + +Next.js currently doesn't provide an API to read route params like `locale` at arbitrary places in Server Components. The `locale` is fundamental to all APIs provided by `next-intl`, therefore passing this as a prop throughout the tree doesn't stand out as particularly ergnomic. + +Due to this, `next-intl` uses its middleware to attach an `x-next-intl-locale` header to the incoming request, holding the negotiated locale as a value. This technique allows the locale to be read at arbitrary places via `headers().get('x-next-intl-locale')`. + +However, the usage of `headers` opts the route into dynamic rendering. + +By using `unstable_setRequestLocale`, you can provide the locale that is received in layouts and pages via `params` to `next-intl`. By doing this, all APIs from `next-intl` can now read from this value instead of the header, enabling static rendering. + +
+ +### 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/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 ba9647505..bc1346933 100644 --- a/docs/pages/docs/getting-started/pages-router.mdx +++ b/docs/pages/docs/getting-started/pages-router.mdx @@ -1,9 +1,9 @@ import {Tab, Tabs} from 'nextra-theme-docs'; import Callout from 'components/Callout'; -# Next.js 13 internationalization (i18n) with the Pages Router +# Next.js 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. @@ -13,10 +13,16 @@ While it's recommended to [use `next-intl` with the App Router](/docs/getting-st ```jsx filename="_app.tsx" /NextIntlClientProvider/ import {NextIntlClientProvider} from 'next-intl'; +import {useRouter} from 'next/router'; export default function App({Component, pageProps}) { + const router = useRouter(); + return ( - + ); @@ -87,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,26 @@ 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 -If you want to include a prefix for the default locale as well, you can configure the middleware accordingly. +#### 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'; @@ -140,15 +150,37 @@ import createMiddleware from 'next-intl/middleware'; export default createMiddleware({ // ... other config - localePrefix: 'always' + localePrefix: 'always' // This is the default }); ``` -In this case, requests without a prefix will be redirected accordingly (e.g. `/about` to `/en/about`). +#### Don't use a locale prefix for the default locale [#locale-prefix-as-needed] -Note that this will affect both prefix-based as well as domain-based routing. +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. -### Never use a locale prefix +```tsx filename="middleware.ts" {6} +import createMiddleware from 'next-intl/middleware'; + +export default createMiddleware({ + // ... other config + + localePrefix: 'as-needed' +}); +``` + +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. + +**Important:** If you use this strategy, you should make sure that [your matcher detects unprefixed pathnames](#matcher-no-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,66 @@ 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: Additional rewrites + +If you need to handle rewrites apart from the ones provided by `next-intl`, you can adjust the `pathname` of the `request` before invoking the `next-intl` middleware (based on ["A/B Testing with Cookies" by Vercel](https://vercel.com/templates/next.js/cookies)). + +This example rewrites requests for `/[locale]/profile` to `/[locale]/profile/new` if a special cookie is set. + +```tsx filename="middleware.ts" +import createIntlMiddleware from 'next-intl/middleware'; +import {NextRequest} from 'next/server'; + +export default async function middleware(request: NextRequest) { + const [, locale, pathname] = request.nextUrl.pathname.split('/'); + + if (pathname === 'profile') { + const usesNewProfile = + (request.cookies.get('NEW_PROFILE')?.value || 'false') === 'true'; + + if (usesNewProfile) { + request.nextUrl.pathname = `/${locale}/profile/new`; + } + } + + const handleI18nRouting = createIntlMiddleware({ + locales: ['en', 'de'], + defaultLocale: 'en' + }); + const response = handleI18nRouting(request); + return response; +} + +export const config = { + 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 +422,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 +447,7 @@ const publicPages = ['/', '/login']; const intlMiddleware = createIntlMiddleware({ locales, + localePrefix: 'as-necssary', defaultLocale: 'en' }); @@ -418,33 +493,28 @@ 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'); } ``` -You can explore a working demo by building [the Next.js 13 example](/examples/app-router) after enabling the static export: +You can explore a working demo by building [the Next.js App Router example](/examples/app-router) after enabling the static export: ```tsx filename="next.config.js" module.exports = { @@ -458,18 +528,15 @@ module.exports = { This can happen either because: -1. The middleware was not set up -2. The middleware was set up in the wrong file (e.g. you're using the `src` folder, but `middleware.ts` was added in the root folder) -3. The middleware matcher didn't match a request, but you're using APIs from `next-intl` in a component - -To address the latter case, you should validate that only valid locales are handled by the `[locale]` layout segment since this effectively acts as a catch-all. - -**Example:** - -1. You use [the recommended matcher](#matcher-config) that excludes all pathnames with a dot (`.`) -2. The user requests a pathname that includes a dot (e.g. `favicon.ico`) -3. If there's no static asset available that matches the request, your layout renders with an invalid `locale` param (i.e. `params.locale === 'favicon.ico'`) +1. The middleware is not set up. +2. The middleware is set up in the wrong file (e.g. you're using the `src` folder, but `middleware.ts` was added in the root folder). +3. The middleware matcher didn't match a request, but you're using APIs from `next-intl` in a component. +4. You're attempting to implement static rendering via [`force-static`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic). -In this case, you want to [interrupt the render and return a 404 status code](/docs/getting-started/app-router-server-components#applocalelayouttsx). +To recover from this error, please make sure that: -Note that if you expect certain pathnames to include a dot (e.g. `/users/jane.doe`), you should ensure that they are [matched by the middleware](#matcher-config). +1. You've [set up the middleware](/docs/getting-started/app-router#middlewarets). +2. You're using APIs from `next-intl` (including [the navigation APIs](/docs/routing/navigation)) exclusively within the `locale` segment. +3. Your [middleware matcher](#matcher-config) matches all routes of your application, including dynamic segments with potentially unexpected characters like dots (e.g. `/users/jane.doe`). +4. If you're using [`localePrefix: 'as-needed'`](#locale-prefix-as-needed), the `locale` segment effectively acts like a catch-all for all unknown routes. You should make sure that `params.locale` is [validated](/docs/getting-started/app-router#applocalelayouttsx) before it's used by any APIs from `next-intl`. +5. To implement static rendering properly, make sure to [provide a static locale](/docs/getting-started/app-router#static-rendering) to `next-intl`. diff --git a/docs/pages/docs/routing/navigation.mdx b/docs/pages/docs/routing/navigation.mdx index 73b7f0f58..ff24b31c2 100644 --- a/docs/pages/docs/routing/navigation.mdx +++ b/docs/pages/docs/routing/navigation.mdx @@ -1,8 +1,7 @@ import Callout from 'components/Callout'; -import VersionTabs from 'components/VersionTabs'; import {Tab, Tabs} from 'nextra-theme-docs'; -# Next.js 13 internationalized navigation +# Next.js App Router internationalized navigation `next-intl` provides drop-in replacements for common Next.js navigation APIs that automatically handle the user locale behind the scenes. @@ -17,47 +16,53 @@ 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: - `/en/about` - `/de/ueber-uns` +**Note:** The terms "shared" and "localized" pathnames are used to refer to pathnames that are created via the file-system based routing in Next.js. If you're using an external system like a CMS to localize pathnames, you'll typically implement this with a catch-all route like `[locale]/[[...slug]]`. + --- -Each strategy will provide you with corresponding [navigation APIs](#apis). +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] With this strategy, the pathnames of your app are identical for all locales. This is the simplest case, because the routes you define in Next.js will map directly to the pathnames that a user can request. -You can import all relevant [navigation APIs](#apis) directly from `next-intl`: +To create [navigation APIs](#apis) for this strategy, use the `createSharedPathnamesNavigation` function: -```tsx -import Link from 'next-intl/link'; -import {usePathname, useRouter} from 'next-intl/client'; -import {redirect} from 'next-intl/server'; +```tsx filename="navigation.ts" +import {createSharedPathnamesNavigation} from 'next-intl/navigation'; + +export const locales = ['en', 'de'] as const; + +export const {Link, redirect, usePathname, useRouter} = + createSharedPathnamesNavigation({locales}); ``` - +The `locales` argument is identical to the configuration that you pass to the middleware. To reuse it there, you can import the `locales` into the middleware. -There's [an open RFC](https://github.com/amannn/next-intl/issues/402) to move all navigation APIs to a single `next-intl/navigation` namespace, providing a factory function that provides strict typing for the supported locales. Leave a comment there if you have feedback! +```tsx filename="middleware.ts" +import createMiddleware from 'next-intl/middleware'; +import {locales} from './navigation'; - +export default createMiddleware({ + defaultLocale: 'en', + locales +}); +``` ### Strategy 2: Localized pathnames [#localized-pathnames] - - This API is only available in [the Server Components - beta](/docs/getting-started/app-router-server-components). - - When using this strategy, you have to provide distinct pathnames for every locale that your app supports. However, the localized variants will be handled by a single route internally, therefore a mapping needs to be provided that is also [consumed by the middleware](/docs/routing/middleware#localizing-pathnames). -You can use the `createLocalizedPathnamesNavigation` function to create corresponding [navigation APIs](#apis) in a central module to easily access the returned functions in your components: +You can use the `createLocalizedPathnamesNavigation` function to create corresponding [navigation APIs](#apis): -```tsx filename="./navigation.ts" +```tsx filename="navigation.ts" import { createLocalizedPathnamesNavigation, Pathnames @@ -111,10 +116,8 @@ export default createMiddleware({ ``` - Have a look at the [App Router example with RSC - support](https://github.com/amannn/next-intl/tree/feat/next-13-rsc/examples/example-next-13) - to explore a working implementation of localized pathnames - ([demo](https://next-intl-example-next-13-git-feat-next-13-rsc-next-intl.vercel.app/)). + Have a look at the [App Router example](/examples/app-router) to explore a + working implementation of localized pathnames. ## APIs @@ -123,11 +126,11 @@ export default createMiddleware({ This component wraps [`next/link`](https://nextjs.org/docs/app/api-reference/components/link) and automatically prefixes the `href` with the current locale as necessary. If the default locale is matched, the `href` remains unchanged and no prefix is added. - + ```tsx -import Link from 'next-intl/link'; +import {Link} from '../navigation'; // When the user is on `/en`, the link will point to `/en/about` About @@ -149,7 +152,7 @@ The [`useSelectedLayoutSegment` hook](https://nextjs.org/docs/app/api-reference/ import {useSelectedLayoutSegment} from 'next/navigation'; import {ComponentProps} from 'react'; -import Link from 'next-intl/link'; +import {Link} from '../navigation'; export default function NavigationLink({ href, @@ -290,7 +293,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} = @@ -303,7 +306,7 @@ export const {Link, redirect, usePathname, useRouter} = - +
How does prefetching of localized links work? @@ -315,12 +318,12 @@ Just like `next/link`, by default all links are prefetched. The one exception to If you need to navigate programmatically, e.g. in an event handler, `next-intl` provides a convience API that wraps [`useRouter` from Next.js](https://nextjs.org/docs/app/api-reference/functions/use-router) and automatically applies the locale of the user. - + ```tsx 'use client'; -import {useRouter} from 'next-intl/client'; +import {useRouter} from '../navigation'; const router = useRouter(); @@ -343,7 +346,7 @@ By combining [`usePathname`](#usepathname) with [`useRouter`](#userouter), you c ```tsx 'use client'; -import {usePathname, useRouter} from 'next-intl/client'; +import {usePathname, useRouter} from '../navigation'; const pathname = usePathname(); const router = useRouter(); @@ -414,19 +417,19 @@ router.replace(
- + ### `usePathname` To retrieve the pathname without a potential locale prefix, you can call `usePathname`. - + ```tsx 'use client'; -import {usePathname} from 'next-intl/client'; +import {usePathname} from '../navigation'; // When the user is on `/en`, this will be `/` const pathname = usePathname(); @@ -449,22 +452,17 @@ const pathname = usePathname(); Note that internal pathnames are returned without params being resolved (e.g. `/users/[userId]`). - + ### `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. - + ```tsx -import {redirect} from 'next-intl/server'; +import {redirect} from '../navigation'; // When the user is on `/en`, this will be `/en/login` redirect('/login'); @@ -498,19 +496,16 @@ redirect({ ``` - + ### `getPathname` If you need to construct a particular pathname based on a locale, you can call the `getPathname` function. This can for example be useful to retrieve a [canonical link](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#alternates) for a page that accepts search params. - + - - 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.) @@ -534,4 +529,4 @@ export async function generateMetadata({params}) { ``` - + diff --git a/docs/pages/docs/usage/_meta.json b/docs/pages/docs/usage/_meta.json index 26eb17591..0c46d39a1 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" } diff --git a/docs/pages/docs/configuration.mdx b/docs/pages/docs/usage/configuration.mdx similarity index 70% rename from docs/pages/docs/configuration.mdx rename to docs/pages/docs/usage/configuration.mdx index 992a323a7..b942064da 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,27 +52,9 @@ export default async function LocaleLayout({children, params: {locale}}) { } ``` - - - -
- 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 @@ -74,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 })); ```
-
+ + +```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 to messages quickly. Additionally, you can [use the shape of your local messages for type checking](/docs/workflows/typescript). +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. @@ -172,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'; @@ -208,34 +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. - - -```tsx - - - -``` +If you prefer to override the default, you can provide an explicit value for `now`: - + -
- 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'; @@ -248,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. @@ -262,16 +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. - + -```js - ({ + formats: { dateTime: { short: { day: 'numeric', @@ -290,23 +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: -```js +```tsx import {useFormatter} from 'next-intl'; function Component() { @@ -353,7 +327,7 @@ Global formats for numbers, dates and times can be referenced in messages too. } ``` -```js +```tsx import {useTranslations} from 'next-intl'; function Component() { @@ -368,26 +342,9 @@ 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. - - -```js - {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'; @@ -399,10 +356,25 @@ export default getRequestConfig(async ({locale}) => ({ // ... })); ``` +
-
+ -The defaults will be overridden by locally provided values. +```tsx + {chunks}, + value: 123 + }} +> + ... + +``` + + + + +The defaults will be overridden if local formats are provided at a specific call site. ## Error handling @@ -410,7 +382,37 @@ By default, when a message fails to resolve or when the formatting failed, an er This behavior can be customized with the `onError` and `getMessageFallback` configuration option. - + + + +```tsx filename="i18n.ts" +import {getRequestConfig} from 'next-intl/server'; +import {IntlErrorCode} from 'next-intl'; + +export default getRequestConfig(async ({locale}) => ({ + 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); + } + }, + 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; + } + }, + // ... +})); +``` + + ```tsx @@ -430,62 +432,54 @@ 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`; + return path + ' is not yet translated'; } else { - return `Dear developer, please fix this message: ${path}`; + return 'Dear developer, please fix this message: ' + path; } } - - + + ... ``` + - - -
- 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'; +## Locale -export default getRequestConfig(async ({locale}) => ({ - 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); - } - }, - getMessageFallback({namespace, key, error}) { - const path = [namespace, key].filter((part) => part != null).join('.'); +`NextIntlClientProvider` requires a `locale` that it provides to all hooks from `next-intl`. - if (error.code === IntlErrorCode.MISSING_MESSAGE) { - return `${path} is not yet translated`; - } else { - return `Dear developer, please fix this message: ${path}`; - } +If you render `NextIntlClientProvider` from a Server Component, the `locale` will automatically be received. + +In all other cases, e.g. when being rendered from a Client Component, a unit test or within the Pages Router, you need to pass this prop explicitly. Be sure to also set a [`timeZone`](#time-zone) and a value for [`now`](#now) in this case to avoid potential markup mismatches. + +
+I'm using the Pages Router, how can I provide the locale? + +If you use [internationalized routing with the Pages Router](https://nextjs.org/docs/pages/building-your-application/routing/internationalization), you can receive the locale from the router in order to pass it to `NextIntlClientProvider`: + +```tsx filename="_app.tsx" +import {useRouter} from 'next/router'; -}, // ... -})); +const router = useRouter(); + +return ( + + ... + ; +); ``` - - +
## Retrieve global configuration As a convenience, there are a couple of hooks that allow you to read global configuration. -```js +```tsx import {useLocale, useTimeZone, useMessages} from 'next-intl'; function Component() { diff --git a/docs/pages/docs/usage/dates-times.mdx b/docs/pages/docs/usage/dates-times.mdx index bae73797a..89f0e4e98 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'; @@ -89,7 +83,7 @@ function Component() { } ``` -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); @@ -192,5 +186,5 @@ t( To reuse date and time formats for multiple components, you can configure - [global formats](/docs/configuration#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 010cb98ee..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#formats). + formats](/docs/usage/configuration#formats).
diff --git a/docs/pages/docs/usage/messages.mdx b/docs/pages/docs/usage/messages.mdx index 46554fc6a..836fdb72d 100644 --- a/docs/pages/docs/usage/messages.mdx +++ b/docs/pages/docs/usage/messages.mdx @@ -7,12 +7,12 @@ 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, a locale can include optional regional information (e.g. `en-US`). Locales are specified as [IETF BCP 47 language tags](https://en.wikipedia.org/wiki/IETF_language_tag). +- **Messages**: These are collections of namespace-label pairs that are grouped 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. +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. You can of course also use a different structure, depending on what suits your app best. ```json filename="en.json" { @@ -22,9 +22,9 @@ To group your messages within a locale, it's recommended to use component names } ``` -Now, you can render messages from within a React component via the `useTranslations` hook: +You can render messages from within a React component via the `useTranslations` hook: -```js filename="About.tsx" +```tsx filename="About.tsx" import {useTranslations} from 'next-intl'; function About() { @@ -60,7 +60,7 @@ Optionally, you can structure your messages as nested objects. } ``` -```js filename="SignUp.tsx" +```tsx filename="SignUp.tsx" import {useTranslations} from 'next-intl'; function SignUp() { @@ -117,9 +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. - -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. +`next-intl` is heavily based on the `useTranslations` API which is intended to consume translations from within React components. This may seem like a 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. If you're interested to dive deeper into this topic, there's a blog post that discusses the background of this design decision: [How (not) to use translations outside of React components](/blog/translations-outside-of-react-components). @@ -165,7 +163,7 @@ t('message', {name: 'Jane'}); // "Hello Jane!" To express the pluralization of a given number of items, the `plural` argument can be used: -```js filename="en.json" +```json filename="en.json" "message": "You have {count, plural, =0 {no followers yet} =1 {one follower} other {# followers}}." ``` @@ -203,7 +201,7 @@ To match a specific number, `next-intl` additionally supports the special `=valu To apply pluralization based on an order of items, the `selectordinal` argument can be used: -```js filename="en.json" +```tsx filename="en.json" "message": "It's your {year, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} birthday!" ``` @@ -235,7 +233,7 @@ To match a specific number, `next-intl` additionally supports the special `=valu To map identifiers to human readable labels, you can use the `select` argument: -```js filename="en.json" +```tsx filename="en.json" "message": "{gender, select, female {She} male {He} other {They}} is online." ``` @@ -268,6 +266,7 @@ You can format rich text with custom tags and map them to React components: ``` ```js +// Returns `<>Please refer to the guidelines.` t.rich('message', { guidelines: (chunks) => {chunks} }); @@ -295,7 +294,7 @@ Messages can use tags without any chunks as children, but syntax-wise, a closing ```js t.rich('message', { - br: (chunks) =>
+ br: () =>
}); ``` @@ -337,57 +336,85 @@ t.rich('message', {
-## Arrays of messages +## HTML markup -If you need to render a list of messages, the recommended approach is to map an array of keys to the corresponding messages: +To render rich text, you typically want to use [rich text formatting](#rich-text). However, if you have a use case where you need to emit raw HTML markup, you can use the `t.markup` function: ```json filename="en.json" { - "Benefits": { - "zero-config": "Works with zero config", - "customizable": "Easy to customize", - "fast": "Blazingly fast" - } + "markup": "This is important" } ``` -```js filename="Benefits.tsx" -import {useTranslations} from 'next-intl'; +```js +// Returns 'This is important' +t.markup('markup', { + important: (chunks) => `${chunks}` +}); +``` -function Benefits() { - const t = useTranslations('Benefits'); - const keys = ['zero-config', 'customizable', 'fast'] as const; +Note that unlike `t.rich`, the provided markup functions accept `chunks` as a `string` and also return a `string` where the `chunks` are wrapped accordingly. - return ( -
    - {keys.map((key) => ( -
  • {t(key)}
  • - ))} -
- ); +## Raw messages + +Messages are always parsed and therefore e.g. for rich text formatting 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: + +```json filename="en.json" +{ + "content": "

Headline

This is raw HTML

" } ``` -If the number of items varies between locales, you can solve this by using [rich text](#rich-text). +```js +
+``` + + + **Important**: You should always sanitize the content that you pass to + [`dangerouslySetInnerHTML`](https://react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html) + to avoid cross-site scripting attacks. + + +The value of a raw message can be any valid JSON value: strings, booleans, objects and arrays. + +## Arrays of messages + +If you need to render a list of messages, the recommended approach is to map an array of keys to the corresponding messages: ```json filename="en.json" { - "Benefits": { - "items": "Works with zero configEasy to customizeBlazingly fast" + "CompanyStats": { + "yearsOfService": { + "title": "Years of service", + "value": "34" + }, + "happyClients": { + "title": "Happy clients", + "value": "1.000+" + }, + "partners": { + "title": "Products", + "value": "5.000+" + } } } ``` -```js filename="Benefits.tsx" +```tsx filename="CompanyStats.tsx" import {useTranslations} from 'next-intl'; -function Benefits() { - const t = useTranslations('Benefits'); +function CompanyStats() { + const t = useTranslations('CompanyStats'); + const keys = ['yearsOfService', 'happyClients', 'partners'] as const; + return (
    - {t.rich('items', { - item: (chunks) =>
  • {chunks}
  • - })} + {keys.map((key) => ( +
  • +

    {t(`${key}.title`)}

    +

    {t(`${key}.value`)}

    +
  • + ))}
); } @@ -408,7 +435,7 @@ The advantage of this approach over supporting arrays in messages is that this w } ``` -```js filename="Benefits.tsx" +```tsx filename="Benefits.tsx" import {useTranslations} from 'next-intl'; function Benefits() { @@ -430,28 +457,6 @@ function Benefits() {

-## Raw messages - -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: - -```json filename="en.json" -{ - "content": "

Headline

This is raw HTML

" -} -``` - -```js -
-``` - - - **Important**: You should always sanitize the content that you pass to - [`dangerouslySetInnerHTML`](https://react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html) - to avoid cross-site scripting attacks. - - -The value of a raw message can be any valid JSON value: strings, booleans, objects and arrays. - ## Right-to-left languages Languages such as Arabic, Hebrew and Persian use [right-to-left script](https://en.wikipedia.org/wiki/Right-to-left_script) (often abbreviated as RTL). For these languages, writing begins on the right side of the page and continues to the left. diff --git a/docs/pages/docs/usage/numbers.mdx b/docs/pages/docs/usage/numbers.mdx index 4353dc9de..a4561713e 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#formats). +To reuse number formats for multiple components, you can configure [global formats](/docs/usage/configuration#formats). diff --git a/docs/pages/docs/workflows/localization-management.mdx b/docs/pages/docs/workflows/localization-management.mdx index 2c25233f9..ea8855b0c 100644 --- a/docs/pages/docs/workflows/localization-management.mdx +++ b/docs/pages/docs/workflows/localization-management.mdx @@ -83,7 +83,7 @@ You can further simplify the process for translators by setting up - -
    -
  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/examples/_meta.json b/docs/pages/examples/_meta.json index 405167b90..4a83ca6f4 100644 --- a/docs/pages/examples/_meta.json +++ b/docs/pages/examples/_meta.json @@ -7,8 +7,8 @@ "title": "Pages Router", "theme": {"layout": "full"} }, - "pages-router-advanced": { - "title": "Pages Router (advanced)", + "app-router-migration": { + "title": "App Router migration", "theme": {"layout": "full"} } } diff --git a/docs/pages/examples/pages-router-advanced.mdx b/docs/pages/examples/app-router-migration.mdx similarity index 54% rename from docs/pages/examples/pages-router-advanced.mdx rename to docs/pages/examples/app-router-migration.mdx index 8fc9e6fea..ede2e4996 100644 --- a/docs/pages/examples/pages-router-advanced.mdx +++ b/docs/pages/examples/app-router-migration.mdx @@ -1,11 +1,11 @@ --- -title: Pages Router (advanced) +title: App Router Migration full: true --- import CodeSandbox from 'components/CodeSandbox'; diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx index a57afbc53..ddaa08c90 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 e2c49c655..ac0a11a02 100644 --- a/docs/theme.config.js +++ b/docs/theme.config.js @@ -74,23 +74,23 @@ export default { project: { link: 'https://github.com/amannn/next-intl' }, + docsRepositoryBase: 'https://github.com/amannn/next-intl/blob/main/docs', + useNextSeoProps() { + return { + titleTemplate: '%s – Internationalization (i18n) for Next.js' + }; + }, banner: { text: ( <> - Try out the{' '} + next-intl 3.0 is out! ( - 3.0 release candidate + announcement - ! + ) ) }, - docsRepositoryBase: 'https://github.com/amannn/next-intl/blob/main/docs', - useNextSeoProps() { - return { - titleTemplate: '%s – Internationalization (i18n) for Next.js' - }; - }, primaryHue: {light: 210, dark: 195}, footer: { component: Footer @@ -154,25 +154,32 @@ export default { + + + - + + + + - - ) }; diff --git a/examples/example-advanced/package.json b/examples/example-advanced/package.json index 95f6db923..caf4cf28e 100644 --- a/examples/example-advanced/package.json +++ b/examples/example-advanced/package.json @@ -23,12 +23,12 @@ "@types/jest": "^29.5.1", "@types/lodash": "^4.14.176", "@types/node": "^17.0.23", - "@types/react": "^18.2.5", + "@types/react": "^18.2.29", "eslint": "^8.46.0", "eslint-config-molindo": "^7.0.0", "eslint-config-next": "^13.4.0", - "jest": "^27.4.5", - "jest-environment-jsdom": "^27.0.0", - "typescript": "^5.0.0" + "jest": "^29.0.0", + "jest-environment-jsdom": "^29.0.0", + "typescript": "^5.2.2" } } diff --git a/examples/example-advanced/src/pages/_app.tsx b/examples/example-advanced/src/pages/_app.tsx index f76456f51..14f75d12d 100644 --- a/examples/example-advanced/src/pages/_app.tsx +++ b/examples/example-advanced/src/pages/_app.tsx @@ -1,4 +1,5 @@ import {AppProps} from 'next/app'; +import {useRouter} from 'next/router'; import {NextIntlClientProvider} from 'next-intl'; type PageProps = { @@ -11,6 +12,8 @@ type Props = Omit, 'pageProps'> & { }; export default function App({Component, pageProps}: Props) { + const router = useRouter(); + return ( + ); diff --git a/examples/example-next-13-advanced/.eslintrc.js b/examples/example-next-13-advanced/.eslintrc.js new file mode 100644 index 000000000..24eb90084 --- /dev/null +++ b/examples/example-next-13-advanced/.eslintrc.js @@ -0,0 +1,13 @@ +module.exports = { + env: { + node: true + }, + extends: [ + 'molindo/typescript', + 'molindo/react', + 'plugin:@next/next/recommended' + ], + rules: { + 'react/react-in-jsx-scope': 'off' + } +}; diff --git a/examples/example-next-13-advanced/.gitignore b/examples/example-next-13-advanced/.gitignore new file mode 100644 index 000000000..04239e7d0 --- /dev/null +++ b/examples/example-next-13-advanced/.gitignore @@ -0,0 +1,4 @@ +/node_modules +/.next/ +.DS_Store +tsconfig.tsbuildinfo diff --git a/examples/example-next-13-advanced/README.md b/examples/example-next-13-advanced/README.md new file mode 100644 index 000000000..73bdf62b8 --- /dev/null +++ b/examples/example-next-13-advanced/README.md @@ -0,0 +1,3 @@ +# example-next-13-advanced + +An example that showcases usage of `next-intl` in the `app` folder of Next.js 13. diff --git a/examples/example-next-13-advanced/global.d.ts b/examples/example-next-13-advanced/global.d.ts new file mode 100644 index 000000000..0e9a1a3de --- /dev/null +++ b/examples/example-next-13-advanced/global.d.ts @@ -0,0 +1,3 @@ +// Declaring this interface provides type safety for message keys +type Messages = typeof import('./messages/en.json'); +declare interface IntlMessages extends Messages {} diff --git a/examples/example-next-13-advanced/messages/de.json b/examples/example-next-13-advanced/messages/de.json new file mode 100644 index 000000000..f2c8a2254 --- /dev/null +++ b/examples/example-next-13-advanced/messages/de.json @@ -0,0 +1,57 @@ +{ + "Index": { + "title": "Start", + "description": "Das ist die Startseite.", + "rich": "Das ist formatierter Test.", + "globalDefaults": "{globalString}" + }, + "AsyncComponent": { + "basic": "AsyncComponent", + "rich": "This is a rich text.", + "markup": "Markup with {globalString}" + }, + "LocaleLayout": { + "title": "next-intl Beispiel", + "description": "Das ist ein Beispiel, wie next-intl im `app`-Verzeichnis verwendet werden kann." + }, + "Client": { + "title": "Client", + "description": "Dise Seite wird auf der Client-Seite initialisiert." + }, + "Nested": { + "title": "Verschachtelt", + "description": "Das ist eine verschachtelte Seite." + }, + "Navigation": { + "home": "Start", + "client": "Client-Seite", + "nested": "Verschachtelte Seite", + "newsArticle": "News-Artikel #{articleId}" + }, + "NewsArticle": { + "title": "News-Artikel #{articleId}" + }, + "NotFound": { + "title": "Diese Seite wurde nicht gefunden (404)" + }, + "LocaleSwitcher": { + "switchLocale": "Zu {locale, select, de {Deutsch} en {Englisch} other {Unbekannt}} wechseln" + }, + "Counter": { + "count": "Aktuell:", + "increment": "Erhöhen" + }, + "ClientCounter": { + "count": "Aktuell: {count}", + "increment": "Erhöhen" + }, + "ApiRoute": { + "hello": "Hallo {name}!" + }, + "OpenGraph": { + "title": "next-intl Beispiel" + }, + "ServerActions": { + "item": "Element #{id}" + } +} diff --git a/examples/example-next-13-advanced/messages/en.json b/examples/example-next-13-advanced/messages/en.json new file mode 100644 index 000000000..cdbbdb93e --- /dev/null +++ b/examples/example-next-13-advanced/messages/en.json @@ -0,0 +1,57 @@ +{ + "Index": { + "title": "Home", + "description": "This is the home page.", + "rich": "This is a rich text.", + "globalDefaults": "{globalString}" + }, + "AsyncComponent": { + "basic": "AsyncComponent", + "rich": "This is a rich text.", + "markup": "Markup with {globalString}" + }, + "LocaleLayout": { + "title": "next-intl example", + "description": "This is an example of using next-intl in the `app` directory." + }, + "Client": { + "title": "Client", + "description": "This page hydrates on the client side." + }, + "Nested": { + "title": "Nested", + "description": "This is a nested page." + }, + "Navigation": { + "home": "Home", + "client": "Client page", + "nested": "Nested page", + "newsArticle": "News article #{articleId}" + }, + "NewsArticle": { + "title": "News article #{articleId}" + }, + "NotFound": { + "title": "This page was not found (404)" + }, + "LocaleSwitcher": { + "switchLocale": "Switch to {locale, select, de {German} en {English} other {Unknown}}" + }, + "Counter": { + "count": "Current count:", + "increment": "Increment" + }, + "ClientCounter": { + "count": "Current count: {count}", + "increment": "Increment" + }, + "ApiRoute": { + "hello": "Hello {name}!" + }, + "OpenGraph": { + "title": "next-intl example" + }, + "ServerActions": { + "item": "Item #{id}" + } +} diff --git a/examples/example-next-13-advanced/messages/es.json b/examples/example-next-13-advanced/messages/es.json new file mode 100644 index 000000000..dc4a979d9 --- /dev/null +++ b/examples/example-next-13-advanced/messages/es.json @@ -0,0 +1,57 @@ +{ + "Index": { + "title": "Inicio", + "description": "Esta es la página de inicio.", + "rich": "Este es un texto importante.", + "globalDefaults": "{globalString}" + }, + "AsyncComponent": { + "basic": "AsyncComponent", + "rich": "This is a rich text.", + "markup": "Markup with {globalString}" + }, + "LocaleLayout": { + "title": "Ejemplo next-intl", + "description": "Este es un ejemplo de cómo usar next-intl en el directorio app." + }, + "Client": { + "title": "Cliente", + "description": "Esta página se hidrata en el lado del cliente." + }, + "Nested": { + "title": "Anidada", + "description": "Esta es una página anidada." + }, + "Navigation": { + "home": "Inicio", + "client": "Página del cliente", + "nested": "Página anidada", + "newsArticle": "Noticias #{articleId}" + }, + "NewsArticle": { + "title": "Noticias #{articleId}" + }, + "NotFound": { + "title": "Esta página no se encontró (404)" + }, + "LocaleSwitcher": { + "switchLocale": "Cambiar a {locale, select, de {Alemán} en {Inglés} other {Desconocido}}" + }, + "Counter": { + "count": "Conteo actual:", + "increment": "Incrementar" + }, + "ClientCounter": { + "count": "Conteo actual: {count}", + "increment": "Incrementar" + }, + "ApiRoute": { + "hello": "¡Hola {name}!" + }, + "OpenGraph": { + "title": "Ejemplo next-intl" + }, + "ServerActions": { + "item": "Artículo #{id}" + } +} diff --git a/examples/example-next-13-advanced/next-env.d.ts b/examples/example-next-13-advanced/next-env.d.ts new file mode 100644 index 000000000..4f11a03dc --- /dev/null +++ b/examples/example-next-13-advanced/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/example-next-13-advanced/next.config.mjs b/examples/example-next-13-advanced/next.config.mjs new file mode 100644 index 000000000..125c56e37 --- /dev/null +++ b/examples/example-next-13-advanced/next.config.mjs @@ -0,0 +1,6 @@ +// @ts-check + +import NextIntlPlugin from 'next-intl/plugin'; + +const withNextIntl = NextIntlPlugin('./src/i18n.tsx'); +export default withNextIntl(); diff --git a/examples/example-next-13-advanced/package.json b/examples/example-next-13-advanced/package.json new file mode 100644 index 000000000..2df98ac55 --- /dev/null +++ b/examples/example-next-13-advanced/package.json @@ -0,0 +1,33 @@ +{ + "name": "example-next-13-advanced", + "version": "2.9.1", + "private": true, + "scripts": { + "dev": "next dev", + "lint": "eslint src && tsc", + "test": "playwright test", + "test:watch": "chokidar 'tests/main.spec.ts' -c 'npm run test'", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "lodash": "^4.17.21", + "ms": "2.1.3", + "next": "14.0.1", + "next-intl": "^2.13.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@playwright/test": "^1.33.0", + "@types/lodash": "^4.14.176", + "@types/node": "^17.0.23", + "@types/react": "^18.2.29", + "chokidar-cli": "3.0.0", + "eslint": "^8.46.0", + "eslint-config-molindo": "7.0.0-alpha.7", + "eslint-config-next": "^13.4.0", + "sharp": "^0.32.6", + "typescript": "^5.2.2" + } +} diff --git a/examples/example-next-13-advanced/playwright.config.ts b/examples/example-next-13-advanced/playwright.config.ts new file mode 100644 index 000000000..2c2f76509 --- /dev/null +++ b/examples/example-next-13-advanced/playwright.config.ts @@ -0,0 +1,24 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import type {PlaywrightTestConfig} from '@playwright/test'; +import {devices} from '@playwright/test'; + +// Use a distinct port on CI to avoid conflicts during concurrent tests +const PORT = process.env.CI ? 3003 : 3000; + +const config: PlaywrightTestConfig = { + retries: process.env.CI ? 1 : 0, + testDir: './tests', + projects: [ + { + name: 'chromium', + use: devices['Desktop Chrome'] + } + ], + webServer: { + command: `PORT=${PORT} pnpm start`, + port: PORT, + reuseExistingServer: true + } +}; + +export default config; diff --git a/examples/example-next-13-advanced/public/assets/image.jpg b/examples/example-next-13-advanced/public/assets/image.jpg new file mode 100644 index 000000000..f9852a89d Binary files /dev/null and b/examples/example-next-13-advanced/public/assets/image.jpg differ diff --git a/examples/example-next-13-advanced/public/favicon.ico b/examples/example-next-13-advanced/public/favicon.ico new file mode 100644 index 000000000..4ddd8fff7 Binary files /dev/null and b/examples/example-next-13-advanced/public/favicon.ico differ diff --git a/examples/example-next-13-advanced/src/app/[locale]/[...rest]/page.tsx b/examples/example-next-13-advanced/src/app/[locale]/[...rest]/page.tsx new file mode 100644 index 000000000..4c9f4064a --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/[...rest]/page.tsx @@ -0,0 +1,9 @@ +import {notFound} from 'next/navigation'; + +export default function CatchAll() { + // `not-found` currently only renders when triggered by the `notFound` function + // https://beta.nextjs.org/docs/api-reference/file-conventions/not-found + notFound(); + + return null; +} diff --git a/examples/example-next-13-advanced/src/app/[locale]/actions/List.tsx b/examples/example-next-13-advanced/src/app/[locale]/actions/List.tsx new file mode 100644 index 000000000..497317785 --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/actions/List.tsx @@ -0,0 +1,38 @@ +'use client'; + +import {ReactNode, useEffect, useState} from 'react'; + +type Props = { + getNextItem(curLength: number): Promise; + title: string; +}; + +export default function List({getNextItem, title}: Props) { + const [isMounted, setIsMounted] = useState(false); + const [items, setItems] = useState>([]); + + function onAddItem() { + getNextItem(items.length).then((item) => { + setItems([...items, item]); + }); + } + + useEffect(() => { + setIsMounted(true); + }, []); + + return ( +
+

{title}

+

{isMounted && 'Mounted'}

+
    + {items.map((item, index) => ( +
  • {item}
  • + ))} +
+ +
+ ); +} diff --git a/examples/example-next-13-advanced/src/app/[locale]/actions/ListItem.tsx b/examples/example-next-13-advanced/src/app/[locale]/actions/ListItem.tsx new file mode 100644 index 000000000..df1a9e5b7 --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/actions/ListItem.tsx @@ -0,0 +1,6 @@ +import {useTranslations} from 'next-intl'; + +export default function ListItem({id}: {id: number}) { + const t = useTranslations('ServerActions'); + return t('item', {id}); +} diff --git a/examples/example-next-13-advanced/src/app/[locale]/actions/ListItemAsync.tsx b/examples/example-next-13-advanced/src/app/[locale]/actions/ListItemAsync.tsx new file mode 100644 index 000000000..801919ba3 --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/actions/ListItemAsync.tsx @@ -0,0 +1,6 @@ +import {getTranslations} from 'next-intl/server'; + +export default async function ListItemAsync({id}: {id: number}) { + const t = await getTranslations('ServerActions'); + return t('item', {id}); +} diff --git a/examples/example-next-13-advanced/src/app/[locale]/actions/ListItemClient.tsx b/examples/example-next-13-advanced/src/app/[locale]/actions/ListItemClient.tsx new file mode 100644 index 000000000..b94cf8749 --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/actions/ListItemClient.tsx @@ -0,0 +1,8 @@ +'use client'; + +import {useTranslations} from 'next-intl'; + +export default function ListItemClient({id}: {id: number}) { + const t = useTranslations('ServerActions'); + return t('item', {id}); +} diff --git a/examples/example-next-13-advanced/src/app/[locale]/actions/page.tsx b/examples/example-next-13-advanced/src/app/[locale]/actions/page.tsx new file mode 100644 index 000000000..8c875e923 --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/actions/page.tsx @@ -0,0 +1,39 @@ +import {pick} from 'lodash'; +import {NextIntlClientProvider, useMessages} from 'next-intl'; +import List from './List'; +import ListItem from './ListItem'; +import ListItemAsync from './ListItemAsync'; +import ListItemClient from './ListItemClient'; + +export default function ServerActions() { + return ( + <> + { + 'use server'; + const id = curLength + 1; + return ; + }} + title="Shared Server Components" + /> + { + 'use server'; + const id = curLength + 1; + return ; + }} + title="Async Server Components" + /> + + { + 'use server'; + const id = curLength + 1; + return ; + }} + title="Client Components" + /> + + + ); +} 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 new file mode 100644 index 000000000..921971072 --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/api/route.ts @@ -0,0 +1,18 @@ +import {NextRequest, NextResponse} from 'next/server'; +import {getTranslations} from 'next-intl/server'; + +type Props = { + params: { + locale: string; + }; +}; + +export async function GET(request: NextRequest, {params: {locale}}: Props) { + const name = request.nextUrl.searchParams.get('name'); + if (!name) { + return new Response('Search param `name` was not provided.', {status: 400}); + } + + 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]/client/ClientContent.tsx b/examples/example-next-13-advanced/src/app/[locale]/client/ClientContent.tsx new file mode 100644 index 000000000..eeebd96a8 --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/client/ClientContent.tsx @@ -0,0 +1,20 @@ +'use client'; + +import {useNow, useTimeZone, useLocale} from 'next-intl'; +import {Link, usePathname} from '../../../navigation'; + +export default function ClientContent() { + const now = useNow(); + const timeZone = useTimeZone(); + const locale = useLocale(); + + return ( + <> +

{now.toISOString()}

+ Go to home +

{usePathname()}

+

{timeZone}

+

{locale}

+ + ); +} diff --git a/examples/example-next-13-advanced/src/app/[locale]/client/DelayedServerContent.tsx b/examples/example-next-13-advanced/src/app/[locale]/client/DelayedServerContent.tsx new file mode 100644 index 000000000..5f8d0989e --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/client/DelayedServerContent.tsx @@ -0,0 +1,13 @@ +import {useNow} from 'next-intl'; +import {use} from 'react'; + +export default function DelayedServerContent() { + use(new Promise((resolve) => setTimeout(resolve, 50))); + const now = useNow(); + + return ( + <> +

{now.toISOString()}

+ + ); +} diff --git a/examples/example-next-13-advanced/src/app/[locale]/client/page.tsx b/examples/example-next-13-advanced/src/app/[locale]/client/page.tsx new file mode 100644 index 000000000..67693fba0 --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/client/page.tsx @@ -0,0 +1,20 @@ +import {useTranslations, useNow, NextIntlClientProvider} from 'next-intl'; +import PageLayout from '../../../components/PageLayout'; +import ClientContent from './ClientContent'; +import DelayedServerContent from './DelayedServerContent'; + +export default function Client() { + const t = useTranslations('Client'); + const now = useNow(); + + return ( + +

{t('description')}

+

{now.toISOString()}

+ + + + +
+ ); +} diff --git a/examples/example-next-13-advanced/src/app/[locale]/client/redirect/page.tsx b/examples/example-next-13-advanced/src/app/[locale]/client/redirect/page.tsx new file mode 100644 index 000000000..2c361746b --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/client/redirect/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import {redirect} from '../../../../navigation'; + +export default function ClientRedirectPage() { + redirect('/client'); +} diff --git a/examples/example-next-13-advanced/src/app/[locale]/layout.tsx b/examples/example-next-13-advanced/src/app/[locale]/layout.tsx new file mode 100644 index 000000000..032388470 --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/layout.tsx @@ -0,0 +1,57 @@ +import {Metadata} from 'next'; +import {notFound} from 'next/navigation'; +import { + getFormatter, + getNow, + getTimeZone, + getTranslations +} from 'next-intl/server'; +import {ReactNode} from 'react'; +import Navigation from '../../components/Navigation'; +import {locales} from '../../navigation'; + +type Props = { + children: ReactNode; + params: {locale: string}; +}; + +export async function generateMetadata({ + params: {locale} +}: Omit): Promise { + const t = await getTranslations({locale, namespace: 'LocaleLayout'}); + const formatter = await getFormatter({locale}); + const now = await getNow({locale}); + const timeZone = await getTimeZone({locale}); + + return { + metadataBase: new URL('http://localhost:3000'), + title: t('title'), + description: t('description'), + other: { + currentYear: formatter.dateTime(now, {year: 'numeric'}), + timeZone: timeZone || 'N/A' + } + }; +} + +export default function LocaleLayout({children, params: {locale}}: Props) { + // Validate that the incoming `locale` parameter is valid + if (!locales.includes(locale as any)) notFound(); + + return ( + + +
+ + {children} +
+ + + ); +} diff --git a/examples/example-next-13-advanced/src/app/[locale]/nested/UnlocalizedPathname.tsx b/examples/example-next-13-advanced/src/app/[locale]/nested/UnlocalizedPathname.tsx new file mode 100644 index 000000000..141a7fab8 --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/nested/UnlocalizedPathname.tsx @@ -0,0 +1,7 @@ +'use client'; + +import {usePathname} from '../../../navigation'; + +export default function UnlocalizedPathname() { + return

{usePathname()}

; +} diff --git a/examples/example-next-13-advanced/src/app/[locale]/nested/page.tsx b/examples/example-next-13-advanced/src/app/[locale]/nested/page.tsx new file mode 100644 index 000000000..b7ce8b5a2 --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/nested/page.tsx @@ -0,0 +1,14 @@ +import {useTranslations} from 'next-intl'; +import PageLayout from '../../../components/PageLayout'; +import UnlocalizedPathname from './UnlocalizedPathname'; + +export default function Nested() { + const t = useTranslations('Nested'); + + return ( + +

{t('description')}

+ +
+ ); +} diff --git a/examples/example-next-13-advanced/src/app/[locale]/news/[articleId]/page.tsx b/examples/example-next-13-advanced/src/app/[locale]/news/[articleId]/page.tsx new file mode 100644 index 000000000..f54f30591 --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/news/[articleId]/page.tsx @@ -0,0 +1,12 @@ +import {useTranslations} from 'next-intl'; + +type Props = { + params: { + articleId: string; + }; +}; + +export default function NewsArticle({params}: Props) { + const t = useTranslations('NewsArticle'); + return

{t('title', {articleId: params.articleId})}

; +} diff --git a/examples/example-next-13-advanced/src/app/[locale]/not-found.tsx b/examples/example-next-13-advanced/src/app/[locale]/not-found.tsx new file mode 100644 index 000000000..77ad0ea1d --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/not-found.tsx @@ -0,0 +1,7 @@ +import {useTranslations} from 'next-intl'; +import PageLayout from '../../components/PageLayout'; + +export default function NotFound() { + const t = useTranslations('NotFound'); + return ; +} 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 new file mode 100644 index 000000000..763972462 --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/opengraph-image.tsx @@ -0,0 +1,13 @@ +import {ImageResponse} from 'next/og'; +import {getTranslations} from 'next-intl/server'; + +type Props = { + params: { + locale: string; + }; +}; + +export default async function Image({params: {locale}}: Props) { + 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 new file mode 100644 index 000000000..74ab3a9b6 --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/page.tsx @@ -0,0 +1,65 @@ +import Image from 'next/image'; +import {useFormatter, useNow, useTimeZone, useTranslations} from 'next-intl'; +import AsyncComponent from '../../components/AsyncComponent'; +import AsyncComponentWithNamespaceAndLocale from '../../components/AsyncComponentWithNamespaceAndLocale'; +import AsyncComponentWithoutNamespace from '../../components/AsyncComponentWithoutNamespace'; +import AsyncComponentWithoutNamespaceAndLocale from '../../components/AsyncComponentWithoutNamespaceAndLocale'; +import ClientLink from '../../components/ClientLink'; +import ClientRouterWithoutProvider from '../../components/ClientRouterWithoutProvider'; +import CoreLibrary from '../../components/CoreLibrary'; +import LocaleSwitcher from '../../components/LocaleSwitcher'; +import PageLayout from '../../components/PageLayout'; +import MessagesAsPropsCounter from '../../components/client/01-MessagesAsPropsCounter'; +import MessagesOnClientCounter from '../../components/client/02-MessagesOnClientCounter'; +import {Link} from '../../navigation'; + +type Props = { + searchParams: Record; +}; + +export default function Index({searchParams}: Props) { + const t = useTranslations('Index'); + const format = useFormatter(); + const now = useNow(); + const timeZone = useTimeZone(); + + return ( + +

{t('description')}

+

+ {t.rich('rich', {important: (chunks) => {chunks}})} +

+

+

{t.rich('globalDefaults')}

+ {/* @ts-expect-error Purposefully trigger an error */} +

{t('missing')}

+

+ {format.dateTime(now, 'medium')} ({timeZone || 'N/A'}) +

+

{format.relativeTime(now)}

+

+ {format.number(23102, {style: 'currency', currency: 'EUR'})} +

+ + + + + +
+ + Go to home with query param + +
+ Link on client without provider +

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

+ + + + + +
+ ); +} diff --git a/examples/example-next-13-advanced/src/app/[locale]/redirect/page.tsx b/examples/example-next-13-advanced/src/app/[locale]/redirect/page.tsx new file mode 100644 index 000000000..520e04565 --- /dev/null +++ b/examples/example-next-13-advanced/src/app/[locale]/redirect/page.tsx @@ -0,0 +1,5 @@ +import {redirect} from '../../../navigation'; + +export default function Redirect() { + redirect('/client'); +} diff --git a/examples/example-next-13-advanced/src/app/layout.tsx b/examples/example-next-13-advanced/src/app/layout.tsx new file mode 100644 index 000000000..b11c9e38b --- /dev/null +++ b/examples/example-next-13-advanced/src/app/layout.tsx @@ -0,0 +1,12 @@ +import {ReactNode} from 'react'; + +type Props = { + children: ReactNode; +}; + +// Even though this component is just passing its children through, the presence +// of this file fixes an issue in Next.js 13.4 where link clicks that switch +// the locale would otherwise cause a full reload. +export default function RootLayout({children}: Props) { + return children; +} diff --git a/examples/example-next-13-advanced/src/components/AsyncComponent.tsx b/examples/example-next-13-advanced/src/components/AsyncComponent.tsx new file mode 100644 index 000000000..5d3626df7 --- /dev/null +++ b/examples/example-next-13-advanced/src/components/AsyncComponent.tsx @@ -0,0 +1,23 @@ +import {getTranslations} from 'next-intl/server'; + +export default async function AsyncComponent() { + const t = await getTranslations('AsyncComponent'); + + return ( +
+

{t('basic')}

+

{t.rich('rich', {important: (chunks) => {chunks}})}

+

{t.markup('markup', {b: (chunks) => `${chunks}`})}

+
+ ); +} + +export async function TypeTest() { + const t = await getTranslations('AsyncComponent'); + + // @ts-expect-error + await getTranslations('Unknown'); + + // @ts-expect-error + t('unknown'); +} diff --git a/examples/example-next-13-advanced/src/components/AsyncComponentWithNamespaceAndLocale.tsx b/examples/example-next-13-advanced/src/components/AsyncComponentWithNamespaceAndLocale.tsx new file mode 100644 index 000000000..7abae580b --- /dev/null +++ b/examples/example-next-13-advanced/src/components/AsyncComponentWithNamespaceAndLocale.tsx @@ -0,0 +1,23 @@ +import {getTranslations, getLocale} from 'next-intl/server'; + +export default async function AsyncComponentWithNamespaceAndLocale() { + const locale = await getLocale(); + const t = await getTranslations({locale, namespace: 'AsyncComponent'}); + + return ( +
+ {t('basic')} +
+ ); +} + +export async function TypeTest() { + const locale = await getLocale(); + const t = await getTranslations({locale}); + + // @ts-expect-error + await getTranslations({locale, namespace: 'Unknown'}); + + // @ts-expect-error + t('AsyncComponent.unknown'); +} diff --git a/examples/example-next-13-advanced/src/components/AsyncComponentWithoutNamespace.tsx b/examples/example-next-13-advanced/src/components/AsyncComponentWithoutNamespace.tsx new file mode 100644 index 000000000..3266c8123 --- /dev/null +++ b/examples/example-next-13-advanced/src/components/AsyncComponentWithoutNamespace.tsx @@ -0,0 +1,18 @@ +import {getTranslations} from 'next-intl/server'; + +export default async function AsyncComponentWithoutNamespace() { + const t = await getTranslations(); + + return ( +
+ {t('AsyncComponent.basic')} +
+ ); +} + +export async function TypeTest() { + const t = await getTranslations(); + + // @ts-expect-error + t('AsyncComponent.unknown'); +} diff --git a/examples/example-next-13-advanced/src/components/AsyncComponentWithoutNamespaceAndLocale.tsx b/examples/example-next-13-advanced/src/components/AsyncComponentWithoutNamespaceAndLocale.tsx new file mode 100644 index 000000000..5896e8da8 --- /dev/null +++ b/examples/example-next-13-advanced/src/components/AsyncComponentWithoutNamespaceAndLocale.tsx @@ -0,0 +1,20 @@ +import {getTranslations, getLocale} from 'next-intl/server'; + +export default async function AsyncComponentWithoutNamespaceAndLocale() { + const locale = await getLocale(); + const t = await getTranslations({locale}); + + return ( +
+ {t('AsyncComponent.basic')} +
+ ); +} + +export async function TypeTest() { + const locale = await getLocale(); + const t = await getTranslations({locale}); + + // @ts-expect-error + t('AsyncComponent.unknown'); +} diff --git a/examples/example-next-13-advanced/src/components/ClientLink.tsx b/examples/example-next-13-advanced/src/components/ClientLink.tsx new file mode 100644 index 000000000..7fbe6dc12 --- /dev/null +++ b/examples/example-next-13-advanced/src/components/ClientLink.tsx @@ -0,0 +1,10 @@ +'use client'; + +import {ComponentProps} from 'react'; +import {Link, pathnames} from '../navigation'; + +export default function NavigationLink( + props: ComponentProps> +) { + return ; +} diff --git a/examples/example-next-13-advanced/src/components/ClientRouterWithoutProvider.tsx b/examples/example-next-13-advanced/src/components/ClientRouterWithoutProvider.tsx new file mode 100644 index 000000000..24aef772f --- /dev/null +++ b/examples/example-next-13-advanced/src/components/ClientRouterWithoutProvider.tsx @@ -0,0 +1,21 @@ +'use client'; + +import {useRouter} from '../navigation'; + +export default function ClientRouterWithoutProvider() { + const router = useRouter(); + + function onClick() { + router.push('/nested'); + } + + return ( + + ); +} diff --git a/examples/example-next-13-advanced/src/components/CoreLibrary.tsx b/examples/example-next-13-advanced/src/components/CoreLibrary.tsx new file mode 100644 index 000000000..eda90ff30 --- /dev/null +++ b/examples/example-next-13-advanced/src/components/CoreLibrary.tsx @@ -0,0 +1,18 @@ +import {createTranslator, createFormatter} from 'next-intl'; + +export default function CoreLibrary() { + const t = createTranslator({ + locale: 'en', + messages: {Index: {title: 'Relative time:'}} as any + }); + + const now = new Date(2022, 10, 6, 20, 20, 0, 0); + const format = createFormatter({locale: 'en', now}); + const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000); + + return ( +

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

+ ); +} diff --git a/examples/example-next-13-advanced/src/components/LocaleSwitcher.tsx b/examples/example-next-13-advanced/src/components/LocaleSwitcher.tsx new file mode 100644 index 000000000..4c241ba44 --- /dev/null +++ b/examples/example-next-13-advanced/src/components/LocaleSwitcher.tsx @@ -0,0 +1,15 @@ +import {useLocale, useTranslations} from 'next-intl'; +import {Link} from '../navigation'; + +export default function LocaleSwitcher() { + const t = useTranslations('LocaleSwitcher'); + + const locale = useLocale(); + const otherLocale = locale === 'en' ? 'de' : 'en'; + + return ( + + {t('switchLocale', {locale: otherLocale})} + + ); +} diff --git a/examples/example-next-13-advanced/src/components/Navigation.tsx b/examples/example-next-13-advanced/src/components/Navigation.tsx new file mode 100644 index 000000000..7a9d43645 --- /dev/null +++ b/examples/example-next-13-advanced/src/components/Navigation.tsx @@ -0,0 +1,19 @@ +import {useTranslations} from 'next-intl'; +import NavigationLink from './NavigationLink'; + +export default function Navigation() { + const t = useTranslations('Navigation'); + + return ( + + ); +} diff --git a/examples/example-next-13-advanced/src/components/NavigationLink.tsx b/examples/example-next-13-advanced/src/components/NavigationLink.tsx new file mode 100644 index 000000000..cffc6a42e --- /dev/null +++ b/examples/example-next-13-advanced/src/components/NavigationLink.tsx @@ -0,0 +1,22 @@ +'use client'; + +import {useSelectedLayoutSegment} from 'next/navigation'; +import {ComponentProps} from 'react'; +import {Link, pathnames} from '../navigation'; + +export default function NavigationLink< + Pathname extends keyof typeof pathnames +>({href, ...rest}: ComponentProps>) { + const selectedLayoutSegment = useSelectedLayoutSegment(); + const pathname = selectedLayoutSegment ? `/${selectedLayoutSegment}` : '/'; + const isActive = pathname === href; + + return ( + + ); +} diff --git a/examples/example-next-13-advanced/src/components/PageLayout.tsx b/examples/example-next-13-advanced/src/components/PageLayout.tsx new file mode 100644 index 000000000..cf5609c4d --- /dev/null +++ b/examples/example-next-13-advanced/src/components/PageLayout.tsx @@ -0,0 +1,15 @@ +import {ReactNode} from 'react'; + +type Props = { + children?: ReactNode; + title: string; +}; + +export default function PageLayout({children, title}: Props) { + return ( +
+

{title}

+ {children} +
+ ); +} diff --git a/examples/example-next-13-advanced/src/components/client/01-MessagesAsPropsCounter/ClientCounter.tsx b/examples/example-next-13-advanced/src/components/client/01-MessagesAsPropsCounter/ClientCounter.tsx new file mode 100644 index 000000000..28c2a9bd5 --- /dev/null +++ b/examples/example-next-13-advanced/src/components/client/01-MessagesAsPropsCounter/ClientCounter.tsx @@ -0,0 +1,29 @@ +'use client'; + +import {useState} from 'react'; + +type Props = { + messages: { + count: string; + increment: string; + }; +}; + +export default function ClientCounter({messages}: Props) { + const [count, setCount] = useState(0); + + function onIncrement() { + setCount(count + 1); + } + + return ( +
+

+ {messages.count} {count} +

+ +
+ ); +} diff --git a/examples/example-next-13-advanced/src/components/client/01-MessagesAsPropsCounter/Counter.tsx b/examples/example-next-13-advanced/src/components/client/01-MessagesAsPropsCounter/Counter.tsx new file mode 100644 index 000000000..d23128f42 --- /dev/null +++ b/examples/example-next-13-advanced/src/components/client/01-MessagesAsPropsCounter/Counter.tsx @@ -0,0 +1,15 @@ +import {useTranslations} from 'next-intl'; +import ClientCounter from './ClientCounter'; + +export default function Counter() { + const t = useTranslations('Counter'); + + return ( + + ); +} diff --git a/examples/example-next-13-advanced/src/components/client/01-MessagesAsPropsCounter/index.tsx b/examples/example-next-13-advanced/src/components/client/01-MessagesAsPropsCounter/index.tsx new file mode 100644 index 000000000..b0723d674 --- /dev/null +++ b/examples/example-next-13-advanced/src/components/client/01-MessagesAsPropsCounter/index.tsx @@ -0,0 +1 @@ +export {default} from './Counter'; diff --git a/examples/example-next-13-advanced/src/components/client/02-MessagesOnClientCounter/ClientCounter.tsx b/examples/example-next-13-advanced/src/components/client/02-MessagesOnClientCounter/ClientCounter.tsx new file mode 100644 index 000000000..e7a013de6 --- /dev/null +++ b/examples/example-next-13-advanced/src/components/client/02-MessagesOnClientCounter/ClientCounter.tsx @@ -0,0 +1,22 @@ +'use client'; + +import {useTranslations} from 'next-intl'; +import {useState} from 'react'; + +export default function ClientCounter() { + const t = useTranslations('ClientCounter'); + const [count, setCount] = useState(0); + + function onIncrement() { + setCount(count + 1); + } + + return ( +
+

{t('count', {count})}

+ +
+ ); +} 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 new file mode 100644 index 000000000..4e3a922be --- /dev/null +++ b/examples/example-next-13-advanced/src/components/client/02-MessagesOnClientCounter/Counter.tsx @@ -0,0 +1,18 @@ +import pick from 'lodash/pick'; +import {NextIntlClientProvider, useMessages} from 'next-intl'; +import ClientCounter from './ClientCounter'; + +export default function Counter() { + const messages = useMessages(); + + return ( + + + + ); +} diff --git a/examples/example-next-13-advanced/src/components/client/02-MessagesOnClientCounter/index.tsx b/examples/example-next-13-advanced/src/components/client/02-MessagesOnClientCounter/index.tsx new file mode 100644 index 000000000..b0723d674 --- /dev/null +++ b/examples/example-next-13-advanced/src/components/client/02-MessagesOnClientCounter/index.tsx @@ -0,0 +1 @@ +export {default} from './Counter'; diff --git a/examples/example-next-13-advanced/src/i18n.tsx b/examples/example-next-13-advanced/src/i18n.tsx new file mode 100644 index 000000000..aff14c5c0 --- /dev/null +++ b/examples/example-next-13-advanced/src/i18n.tsx @@ -0,0 +1,45 @@ +import {headers} from 'next/headers'; +import {getRequestConfig} from 'next-intl/server'; + +export default getRequestConfig(async ({locale}) => { + const now = headers().get('x-now'); + const timeZone = headers().get('x-time-zone') ?? 'Europe/Vienna'; + const messages = (await import(`../messages/${locale}.json`)).default; + + return { + now: now ? new Date(now) : undefined, + timeZone, + messages, + defaultTranslationValues: { + globalString: 'Global string', + highlight: (chunks) => {chunks} + }, + formats: { + dateTime: { + medium: { + dateStyle: 'medium', + timeStyle: 'short', + hour12: false + } + } + }, + onError(error) { + if ( + error.message === + (process.env.NODE_ENV === 'production' + ? 'MISSING_MESSAGE' + : 'MISSING_MESSAGE: Could not resolve `missing` in `Index`.') + ) { + // Do nothing, this error is triggered on purpose + } else { + console.error(JSON.stringify(error.message)); + } + }, + getMessageFallback({key, namespace}) { + return ( + '`getMessageFallback` called for ' + + [namespace, key].filter((part) => part != null).join('.') + ); + } + }; +}); diff --git a/examples/example-next-13-advanced/src/middleware.ts b/examples/example-next-13-advanced/src/middleware.ts new file mode 100644 index 000000000..4006e7a92 --- /dev/null +++ b/examples/example-next-13-advanced/src/middleware.ts @@ -0,0 +1,14 @@ +import createMiddleware from 'next-intl/middleware'; +import {locales, pathnames} from './navigation'; + +export default createMiddleware({ + defaultLocale: 'en', + localePrefix: 'as-needed', + pathnames, + locales +}); + +export const config = { + // Skip all paths that should not be internationalized + matcher: ['/((?!_next|.*\\..*).*)'] +}; diff --git a/examples/example-next-13-advanced/src/navigation.tsx b/examples/example-next-13-advanced/src/navigation.tsx new file mode 100644 index 000000000..6cf704620 --- /dev/null +++ b/examples/example-next-13-advanced/src/navigation.tsx @@ -0,0 +1,29 @@ +import { + createLocalizedPathnamesNavigation, + Pathnames +} from 'next-intl/navigation'; + +export const locales = ['en', 'de', 'es'] as const; + +export const pathnames = { + '/': '/', + '/client': '/client', + '/client/redirect': '/client/redirect', + '/nested': { + en: '/nested', + de: '/verschachtelt', + es: '/anidada' + }, + '/redirect': '/redirect', + '/news/[articleId]': { + en: '/news/[articleId]', + de: '/neuigkeiten/[articleId]', + es: '/noticias/[articleId]' + } +} satisfies Pathnames; + +export const {Link, redirect, usePathname, useRouter} = + createLocalizedPathnamesNavigation({ + locales, + pathnames + }); diff --git a/examples/example-next-13-advanced/tests/main.spec.ts b/examples/example-next-13-advanced/tests/main.spec.ts new file mode 100644 index 000000000..232c2cf48 --- /dev/null +++ b/examples/example-next-13-advanced/tests/main.spec.ts @@ -0,0 +1,593 @@ +import {test as it, expect, Page, BrowserContext} from '@playwright/test'; + +async function assertLocaleCookieValue(page: Page, value: string) { + await expect(async () => { + const cookie = (await page.context().cookies()).find( + (cur) => cur.name === 'NEXT_LOCALE' + ); + expect(cookie).toMatchObject({ + name: 'NEXT_LOCALE', + value + }); + }).toPass(); +} + +function getPageLoadTracker(context: BrowserContext) { + const state = {numPageLoads: 0}; + + context.on('request', (request) => { + // Is the same in dev and prod + if (request.url().includes('/chunks/main-app')) { + state.numPageLoads++; + } + }); + + return state; +} + +it('handles unknown locales', async ({page}) => { + const response = await page.goto('/unknown'); + expect(response?.status()).toBe(404); + await expect(page).toHaveURL('/unknown'); + await expect( + page.getByRole('heading', {name: 'This page was not found (404)'}) + ).toBeVisible(); +}); + +it('redirects to a matched locale at the root for non-default locales', async ({ + browser +}) => { + const context = await browser.newContext({locale: 'de'}); + const page = await context.newPage(); + + await page.goto('/'); + await expect(page).toHaveURL('/de'); + page.getByRole('heading', {name: 'Start'}); +}); + +it('redirects a prefixed pathname for the default locale to the unprefixed version', async ({ + request +}) => { + const response = await request.get('/en', { + maxRedirects: 0 + }); + expect(response.status()).toBe(307); + expect(response.headers().location).toEqual('/'); +}); + +it('redirects a more specific locale to a more generic one', async ({ + browser +}) => { + const context = await browser.newContext({locale: 'de-DE'}); + const page = await context.newPage(); + + await page.goto('/'); + await expect(page).toHaveURL('/de'); + page.getByRole('heading', {name: 'Start'}); +}); + +it('does not redirect on the root if the default locale is matched', async ({ + page +}) => { + await page.goto('/'); + await expect(page).toHaveURL('/'); + page.getByRole('heading', {name: 'Home'}); +}); + +it('supports unprefixed routing for the default locale', async ({page}) => { + await page.goto('/de/verschachtelt'); + await expect(page).toHaveURL('/de/verschachtelt'); + page.getByRole('heading', {name: 'Verschachtelt'}); +}); + +it('supports prefixed routing for non-default locales', async ({page}) => { + await page.goto('/nested'); + await expect(page).toHaveURL('/nested'); + page.getByRole('heading', {name: 'Nested'}); +}); + +it('redirects unprefixed paths for non-default locales', async ({browser}) => { + const context = await browser.newContext({locale: 'de'}); + const page = await context.newPage(); + + await page.goto('/nested'); + await expect(page).toHaveURL('/de/verschachtelt'); + page.getByRole('heading', {name: 'Verschachtelt'}); +}); + +it('remembers the last locale', async ({page}) => { + await page.goto('/de'); + + // Wait for the cookie to be set on the client side + await assertLocaleCookieValue(page, 'de'); + + await page.goto('/'); + await expect(page).toHaveURL('/de'); +}); + +it('sets the `lang` attribute on `html`', async ({page}) => { + await page.goto('/en'); + await page.waitForSelector('html[lang="en"]'); +}); + +it('can be used in the head', async ({page}) => { + await page.goto('/en'); + await expect(page).toHaveTitle('next-intl example'); + + await page.goto('/de'); + await expect(page).toHaveTitle('next-intl Beispiel'); +}); + +it('can be used to localize the page', async ({page}) => { + await page.goto('/en'); + page.locator('text=This is the home page.'); + + await page.goto('/de'); + page.locator('text=Das ist die Startseite.'); +}); + +it('can pass internationalized labels to a client component', async ({ + page +}) => { + await page.goto('/en'); + const element = page.getByTestId('MessagesAsPropsCount'); + await expect(element).toHaveText(/Current count: 0/); + await element.getByRole('button', {name: 'Increment'}).click(); + await expect(element).toHaveText(/Current count: 1/); +}); + +it('can use next-intl on the client side', async ({page}) => { + await page.goto('/en'); + const element = page.getByTestId('MessagesOnClientCounter'); + await expect(element).toHaveText(/Current count: 0/); + await element.getByRole('button', {name: 'Increment'}).click(); + await expect(element).toHaveText(/Current count: 1/); +}); + +it('can use rich text', async ({page}) => { + await page.goto('/en'); + const element = page.getByTestId('RichText'); + expect(await element.innerHTML()).toBe('This is a rich text.'); +}); + +it('can use raw text', async ({page}) => { + await page.goto('/en'); + const element = page.getByTestId('RawText'); + expect(await element.innerHTML()).toBe( + 'This is a rich text.' + ); +}); + +it('can use global defaults', async ({page}) => { + await page.goto('/en'); + const element = page.getByTestId('GlobalDefaults'); + expect(await element.innerHTML()).toBe('Global string'); +}); + +it('can use `getMessageFallback`', async ({page}) => { + await page.goto('/en'); + const element = page.getByTestId('MissingMessage'); + expect(await element.innerHTML()).toBe( + '`getMessageFallback` called for Index.missing' + ); +}); + +it('can use the core library', async ({page}) => { + await page.goto('/en'); + const element = page.getByTestId('CoreLibrary'); + await expect(element).toHaveText('Relative time: tomorrow'); +}); + +it('can use `Link` on the server', async ({page}) => { + await page.goto('/'); + await expect(page.getByRole('link', {name: /^Home$/})).toHaveAttribute( + 'href', + '/' + ); + await expect(page.getByRole('link', {name: 'Nested page'})).toHaveAttribute( + 'href', + '/nested' + ); +}); + +it('can use `Link` with an object as `href`', async ({page}) => { + await page.goto('/'); + const link = page.getByRole('link', {name: 'Go to home with query param'}); + await expect(link).toHaveAttribute('href', '/?test=true'); + await link.click(); + await expect(page).toHaveURL('/?test=true'); +}); + +it('can use `Link` to link to the root of another language', async ({page}) => { + await page.goto('/'); + const link = page.getByRole('link', {name: 'Switch to German'}); + await expect(link).toHaveAttribute('href', '/de'); + await link.click(); + await expect(page).toHaveURL('/de'); + await page.getByRole('link', {name: 'Zu Englisch wechseln'}).click(); + await expect(page).toHaveURL('/'); +}); + +it('uses client-side transitions when using link', async ({context, page}) => { + const tracker = getPageLoadTracker(context); + + await page.goto('/'); + expect(tracker.numPageLoads).toBe(1); + + await page.getByRole('link', {name: 'Nested page'}).click(); + await expect(page).toHaveURL('/nested'); + expect(tracker.numPageLoads).toBe(1); + + await page.getByRole('link', {name: 'Client page'}).click(); + await expect(page).toHaveURL('/client'); + expect(tracker.numPageLoads).toBe(1); + + await page.goBack(); + await expect(page).toHaveURL('/nested'); + expect(tracker.numPageLoads).toBe(1); + + await page.goForward(); + await expect(page).toHaveURL('/client'); + expect(tracker.numPageLoads).toBe(1); +}); + +it('keeps the locale cookie updated when changing the locale and uses soft navigation (no reloads)', async ({ + context, + page +}) => { + const tracker = getPageLoadTracker(context); + + await page.goto('/'); + await assertLocaleCookieValue(page, 'en'); + expect(tracker.numPageLoads).toBe(1); + + const link = page.getByRole('link', {name: 'Switch to German'}); + await link.hover(); + await assertLocaleCookieValue(page, 'en'); + await link.click(); + + await expect(page).toHaveURL('/de'); + await assertLocaleCookieValue(page, 'de'); + + // Currently, a root layout outside of the `[locale]` + // folder is required for this to work. + expect(tracker.numPageLoads).toBe(1); +}); + +it('can use `Link` in client components without using a provider', async ({ + page +}) => { + await page.goto('/'); + await expect( + page.getByRole('link', {name: 'Link on client without provider'}) + ).toHaveAttribute('href', '/'); + + await page.goto('/de'); + await expect( + page.getByRole('link', {name: 'Link on client without provider'}) + ).toHaveAttribute('href', '/de'); +}); + +it('can use `Link` on the client', async ({page}) => { + await page.goto('/client'); + await expect(page.getByRole('link', {name: 'Go to home'})).toHaveAttribute( + 'href', + '/' + ); + + await page.goto('/de/client'); + await expect(page.getByRole('link', {name: 'Go to home'})).toHaveAttribute( + 'href', + '/de' + ); +}); + +it('prefixes as necessary with `Link`', async ({page}) => { + await page.goto('/'); + await expect(page.getByRole('link', {name: /^Home$/})).toHaveAttribute( + 'href', + '/' + ); + await expect(page.getByRole('link', {name: 'Client page'})).toHaveAttribute( + 'href', + '/client' + ); + await expect( + page.getByRole('link', {name: 'Switch to German'}) + ).toHaveAttribute('href', '/de'); + + await page.goto('/en'); + await expect(page.getByRole('link', {name: /^Home$/})).toHaveAttribute( + 'href', + '/' + ); + await expect(page.getByRole('link', {name: 'Client page'})).toHaveAttribute( + 'href', + '/client' + ); + await expect( + page.getByRole('link', {name: 'Switch to German'}) + ).toHaveAttribute('href', '/de'); + + await page.goto('/de'); + await expect(page.getByRole('link', {name: /^Start$/})).toHaveAttribute( + 'href', + '/de' + ); + await expect(page.getByRole('link', {name: 'Client-Seite'})).toHaveAttribute( + 'href', + '/de/client' + ); + await expect( + page.getByRole('link', {name: 'Zu Englisch wechseln'}) + ).toHaveAttribute('href', '/en'); +}); + +it('supports a consistent `now` value across the server and client', async ({ + page +}) => { + await page.goto('/en/client'); + + const serverDate = await page.getByTestId('NowFromServer').textContent(); + const serverDateDelayed = await page + .getByTestId('NowFromServerDelayed') + .textContent(); + const clientDate = await page.getByTestId('NowFromClient').textContent(); + + expect(serverDate).toBe(serverDateDelayed); + expect(serverDate).toBe(clientDate); +}); + +it('can use `usePathname`', async ({page}) => { + await page.goto('/client'); + await expect(page.getByTestId('UnlocalizedPathname')).toHaveText('/client'); + + await page.goto('/en/client'); + await expect(page.getByTestId('UnlocalizedPathname')).toHaveText('/client'); + + await page.goto('/de/client'); + await expect(page.getByTestId('UnlocalizedPathname')).toHaveText('/client'); +}); + +it('can use `usePathname` to get internal pathnames', async ({page}) => { + await page.goto('/de/verschachtelt'); + await expect(page.getByTestId('UnlocalizedPathname')).toHaveText('/nested'); + + await page.goto('/en/nested'); + await expect(page.getByTestId('UnlocalizedPathname')).toHaveText('/nested'); +}); + +it('returns the correct value from `usePathname` in the initial render', async ({ + request +}) => { + expect(await (await request.get('/client')).text()).toContain( + '

/client

' + ); + expect(await (await request.get('/de/client')).text()).toContain( + '

/client

' + ); +}); + +it('can use `redirect` in Server Components', async ({page}) => { + await page.goto('/redirect'); + await expect(page).toHaveURL('/client'); + + await page.goto('/de/redirect'); + await expect(page).toHaveURL('/de/client'); +}); + +it('can use `redirect` in Client Components', async ({page}) => { + await page.goto('/client/redirect'); + await expect(page).toHaveURL('/client'); + + await page.goto('/de/client/redirect'); + await expect(page).toHaveURL('/de/client'); +}); + +it('can navigate between sibling pages that share a parent layout', async ({ + page +}) => { + await page.goto('/nested'); + await page.getByRole('link', {name: 'Client page'}).click(); + await expect(page).toHaveURL('/client'); + await page.getByRole('link', {name: 'Nested page'}).click(); + await expect(page).toHaveURL('/nested'); +}); + +it('prefixes routes as necessary with the router', async ({page}) => { + await page.goto('/'); + page.getByTestId('ClientRouterWithoutProvider-link').click(); + await expect(page).toHaveURL('/nested'); + + await page.goto('/en'); + page.getByTestId('ClientRouterWithoutProvider-link').click(); + await expect(page).toHaveURL('/nested'); + + await page.goto('/de'); + page.getByTestId('ClientRouterWithoutProvider-link').click(); + await expect(page).toHaveURL('/de/verschachtelt'); +}); + +it('can set `now` and `timeZone` at runtime', async ({page}) => { + page.setExtraHTTPHeaders({ + 'x-now': '2020-01-01T00:00:00.000Z', + 'x-time-zone': 'Asia/Shanghai' + }); + + await page.goto('/en'); + const element = page.getByTestId('CurrentTime'); + await expect(element).toHaveText('Jan 1, 2020, 08:00 (Asia/Shanghai)'); +}); + +it('automatically inherits a time zone and locale on the client side when using the provider in an RSC', async ({ + page +}) => { + await page.goto('/client'); + await expect(page.getByTestId('TimeZone')).toHaveText('Europe/Vienna'); + await expect(page.getByTestId('Locale')).toHaveText('en'); +}); + +it('keeps search params for directly matched pages', async ({page}) => { + await page.goto('/de?param=true'); + await expect(page).toHaveURL('/de?param=true'); + await expect(page.getByTestId('SearchParams')).toHaveText( + '{ "param": "true" }' + ); +}); + +it('keeps search params for rewrites', async ({page}) => { + await page.goto('/?param=true'); + await expect(page).toHaveURL('/?param=true'); + await expect(page.getByTestId('SearchParams')).toHaveText( + '{ "param": "true" }' + ); +}); + +it('keeps search params for redirects', async ({browser}) => { + const context = await browser.newContext({locale: 'de-DE'}); + const page = await context.newPage(); + + await page.goto('/?param=true'); + await expect(page).toHaveURL('/de?param=true'); + await expect(page.getByTestId('SearchParams')).toHaveText( + '{ "param": "true" }' + ); +}); + +it('sets alternate links', async ({request}) => { + async function getLinks(pathname: string) { + return ( + (await request.get(pathname)) + .headers() + .link.split(', ') + // On CI, Playwright uses a different host somehow + .map((cur) => cur.replace(/0\.0\.0\.0/g, 'localhost')) + // Normalize ports + .map((cur) => cur.replace(/localhost:\d{4}/g, 'localhost:3000')) + ); + } + + for (const pathname of ['/', '/en', '/de']) { + expect(await getLinks(pathname)).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="es"', + '; rel="alternate"; hreflang="x-default"' + ]); + } + + for (const pathname of ['/nested', '/en/nested', '/de/nested']) { + expect(await getLinks(pathname)).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="es"', + '; rel="alternate"; hreflang="x-default"' + ]); + } +}); + +it('can use rewrites to localize pathnames', async ({page}) => { + await page.goto('/de/verschachtelt'); + page.getByRole('heading', {name: 'Verschachtelt'}); + + // Dynamic params + await page.goto('/en/news/3'); + await expect(page).toHaveURL('/news/3'); + page.getByRole('heading', {name: 'News article #3'}); + await page.goto('/de/neuigkeiten/3'); + await expect(page).toHaveURL('/de/neuigkeiten/3'); + page.getByRole('heading', {name: 'News-Artikel #3'}); + + // Automatic redirects + await page.goto('/de/nested'); + await expect(page).toHaveURL('/de/verschachtelt'); + page.getByRole('heading', {name: 'Verschachtelt'}); + await page.goto('/en/verschachtelt'); + await expect(page).toHaveURL('/nested'); + page.getByRole('heading', {name: 'Nested'}); + await page.goto('/en/neuigkeiten/3'); + await expect(page).toHaveURL('/news/3'); + page.getByRole('heading', {name: 'News article #3'}); +}); + +it('replaces invalid cookie locales', async ({page}) => { + page.setExtraHTTPHeaders({ + cookie: 'NEXT_LOCALE=zh' + }); + await page.goto('/'); + assertLocaleCookieValue(page, 'en'); +}); + +it('can localize route handlers', async ({request}) => { + // Default + { + const response = await request.get('/api?name=world'); + expect(response.status()).toBe(200); + const data = await response.json(); + expect(data).toEqual({message: 'Hello world!'}); + } + + // German + { + const response = await request.get('/de/api?name=Welt'); + expect(response.status()).toBe(200); + const data = await response.json(); + expect(data).toEqual({message: 'Hallo Welt!'}); + } +}); + +it('can use the formatter', async ({page}) => { + await page.goto('/en'); + await expect(page.getByTestId('CurrentTimeRelative')).toHaveText('now'); + await expect(page.getByTestId('Number')).toHaveText('€23,102.00'); +}); + +it('populates metadata', async ({page}) => { + await page.goto('/en'); + await expect(page).toHaveTitle('next-intl example'); + await expect(page.locator('meta[name="description"]')).toHaveAttribute( + 'content', + 'This is an example of using next-intl in the `app` directory.' + ); + await expect(page.locator('meta[name="currentYear"]')).toHaveAttribute( + 'content', + new Date().getFullYear().toString() + ); + await expect(page.locator('meta[name="timeZone"]')).toHaveAttribute( + 'content', + 'Europe/Vienna' + ); +}); + +it('supports opengraph images', async ({page, request}) => { + await page.goto('/'); + const ogImage = await page + .locator('meta[property="og:image"]') + .getAttribute('content'); + expect(ogImage).toBeTruthy(); + const ogImageUrl = new URL(ogImage!); + expect(ogImageUrl.pathname).toBe('/en/opengraph-image'); + const result = await request.get(ogImageUrl.pathname); + expect(result.ok()).toBe(true); +}); + +it('can use async APIs in async components', async ({page}) => { + await page.goto('/'); + + const element1 = page.getByTestId('AsyncComponent'); + element1.getByText('AsyncComponent'); + expect(await element1.innerHTML()).toContain('This is a rich text.'); + element1.getByText('Markup with Global string'); + + page + .getByTestId('AsyncComponentWithoutNamespace') + .getByText('AsyncComponent'); + + page + .getByTestId('AsyncComponentWithNamespaceAndLocale') + .getByText('AsyncComponent'); + + page + .getByTestId('AsyncComponentWithoutNamespaceAndLocale') + .getByText('AsyncComponent'); +}); diff --git a/examples/example-next-13-advanced/tsconfig.json b/examples/example-next-13-advanced/tsconfig.json new file mode 100644 index 000000000..1556449fd --- /dev/null +++ b/examples/example-next-13-advanced/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "eslint-config-molindo/tsconfig.json", + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/examples/example-next-13-next-auth/next.config.js b/examples/example-next-13-next-auth/next.config.js new file mode 100644 index 000000000..d9797bdf4 --- /dev/null +++ b/examples/example-next-13-next-auth/next.config.js @@ -0,0 +1,3 @@ +const withNextIntl = require('next-intl/plugin')(); + +module.exports = withNextIntl(); diff --git a/examples/example-next-13-next-auth/package.json b/examples/example-next-13-next-auth/package.json index f912ecf29..8206ebc23 100644 --- a/examples/example-next-13-next-auth/package.json +++ b/examples/example-next-13-next-auth/package.json @@ -17,13 +17,13 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@playwright/test": "^1.28.1", + "@playwright/test": "^1.33.0", "@types/lodash": "^4.14.176", "@types/node": "^17.0.23", - "@types/react": "^18.2.5", + "@types/react": "^18.2.29", "eslint": "^8.46.0", "eslint-config-molindo": "^7.0.0", "eslint-config-next": "^13.4.0", - "typescript": "^5.0.0" + "typescript": "^5.2.2" } } diff --git a/examples/example-next-13-next-auth/playwright.config.ts b/examples/example-next-13-next-auth/playwright.config.ts index 6515d6b31..cdf3257a6 100644 --- a/examples/example-next-13-next-auth/playwright.config.ts +++ b/examples/example-next-13-next-auth/playwright.config.ts @@ -6,6 +6,7 @@ import {devices} from '@playwright/test'; const PORT = process.env.CI ? 3002 : 3000; const config: PlaywrightTestConfig = { + retries: process.env.CI ? 1 : 0, testDir: './tests', projects: [ { 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/i18n.ts b/examples/example-next-13-next-auth/src/i18n.ts new file mode 100644 index 000000000..c45ddee14 --- /dev/null +++ b/examples/example-next-13-next-auth/src/i18n.ts @@ -0,0 +1,5 @@ +import {getRequestConfig} from 'next-intl/server'; + +export default getRequestConfig(async ({locale}) => ({ + messages: (await import(`../messages/${locale}.json`)).default +})); diff --git a/examples/example-next-13-next-auth/src/middleware.tsx b/examples/example-next-13-next-auth/src/middleware.ts similarity index 94% rename from examples/example-next-13-next-auth/src/middleware.tsx rename to examples/example-next-13-next-auth/src/middleware.ts index 21c8605fb..fb6202ee5 100644 --- a/examples/example-next-13-next-auth/src/middleware.tsx +++ b/examples/example-next-13-next-auth/src/middleware.ts @@ -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/.eslintrc.js b/examples/example-next-13-with-pages/.eslintrc.js new file mode 100644 index 000000000..24eb90084 --- /dev/null +++ b/examples/example-next-13-with-pages/.eslintrc.js @@ -0,0 +1,13 @@ +module.exports = { + env: { + node: true + }, + extends: [ + 'molindo/typescript', + 'molindo/react', + 'plugin:@next/next/recommended' + ], + rules: { + 'react/react-in-jsx-scope': 'off' + } +}; diff --git a/examples/example-next-13-with-pages/.gitignore b/examples/example-next-13-with-pages/.gitignore new file mode 100644 index 000000000..ee91e9ab6 --- /dev/null +++ b/examples/example-next-13-with-pages/.gitignore @@ -0,0 +1,7 @@ +/node_modules +/.next/ +.DS_Store +tsconfig.tsbuildinfo +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/examples/example-next-13-with-pages/README.md b/examples/example-next-13-with-pages/README.md new file mode 100644 index 000000000..07283c936 --- /dev/null +++ b/examples/example-next-13-with-pages/README.md @@ -0,0 +1,12 @@ +# example-next-13-with-pages + +An example that showcases a basic installation of `next-intl` in a Next.js app that uses both the Pages as well as the App Router. + +**Important**: Do not add an `i18n` config to `next.config.js`, as it will cause the `app` directory to be ignored. + +You can run the example locally like this: + +``` +yarn +yarn dev +``` diff --git a/examples/example-next-13-with-pages/messages/de.json b/examples/example-next-13-with-pages/messages/de.json new file mode 100644 index 000000000..c7e7940ec --- /dev/null +++ b/examples/example-next-13-with-pages/messages/de.json @@ -0,0 +1,14 @@ +{ + "Index": { + "title": "Start", + "description": "Das ist die Startseite.", + "navigateToAbout": "Zu Über uns navigieren" + }, + "About": { + "title": "Über", + "description": "Das ist die Über-uns-Seite." + }, + "LocaleSwitcher": { + "switchLocale": "Zu {locale, select, de {Deutsch} en {Englisch} other {Unbekannt}} wechseln" + } +} diff --git a/examples/example-next-13-with-pages/messages/en.json b/examples/example-next-13-with-pages/messages/en.json new file mode 100644 index 000000000..3ef0a509d --- /dev/null +++ b/examples/example-next-13-with-pages/messages/en.json @@ -0,0 +1,14 @@ +{ + "Index": { + "title": "Home", + "description": "This is the home page.", + "navigateToAbout": "Navigate to about" + }, + "About": { + "title": "About", + "description": "This is the about page." + }, + "LocaleSwitcher": { + "switchLocale": "Switch to {locale, select, de {German} en {English} other {Unknown}}" + } +} diff --git a/examples/example-next-13-with-pages/next-env.d.ts b/examples/example-next-13-with-pages/next-env.d.ts new file mode 100644 index 000000000..fd36f9494 --- /dev/null +++ b/examples/example-next-13-with-pages/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/example-next-13-with-pages/next.config.js b/examples/example-next-13-with-pages/next.config.js new file mode 100644 index 000000000..d9797bdf4 --- /dev/null +++ b/examples/example-next-13-with-pages/next.config.js @@ -0,0 +1,3 @@ +const withNextIntl = require('next-intl/plugin')(); + +module.exports = withNextIntl(); diff --git a/examples/example-next-13-with-pages/package.json b/examples/example-next-13-with-pages/package.json new file mode 100644 index 000000000..4329bdc24 --- /dev/null +++ b/examples/example-next-13-with-pages/package.json @@ -0,0 +1,27 @@ +{ + "name": "example-next-13-with-pages", + "version": "2.10.2", + "private": true, + "scripts": { + "dev": "next dev", + "lint": "eslint src && tsc", + "test": "echo 'No tests yet'", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "14.0.1", + "next-intl": "^2.13.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/lodash": "^4.14.176", + "@types/node": "^17.0.23", + "@types/react": "^18.2.29", + "eslint": "^8.46.0", + "eslint-config-molindo": "7.0.0-alpha.7", + "eslint-config-next": "^13.4.0", + "typescript": "^5.2.2" + } +} diff --git a/examples/example-next-13-with-pages/public/favicon.ico b/examples/example-next-13-with-pages/public/favicon.ico new file mode 100644 index 000000000..4ddd8fff7 Binary files /dev/null and b/examples/example-next-13-with-pages/public/favicon.ico differ diff --git a/examples/example-next-13-with-pages/src/app/[locale]/layout.tsx b/examples/example-next-13-with-pages/src/app/[locale]/layout.tsx new file mode 100644 index 000000000..479d16458 --- /dev/null +++ b/examples/example-next-13-with-pages/src/app/[locale]/layout.tsx @@ -0,0 +1,22 @@ +/* eslint-disable @next/next/no-head-element */ +import {useLocale} from 'next-intl'; +import {ReactNode} from 'react'; + +type Props = { + children: ReactNode; + params: {locale: string}; +}; + +export default async function LocaleLayout({children}: Props) { + const locale = useLocale(); + + return ( + + + next-intl + + + {children} + + ); +} 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 new file mode 100644 index 000000000..131116dc4 --- /dev/null +++ b/examples/example-next-13-with-pages/src/app/[locale]/page.tsx @@ -0,0 +1,21 @@ +import {useLocale, useTranslations} from 'next-intl'; +import LocaleSwitcher from '../../components/LocaleSwitcher'; +import PageLayout from '../../components/PageLayout'; +import {Link} from '../../navigation'; + +export default function Index() { + const t = useTranslations('Index'); + const locale = useLocale(); + + return ( + +

{t('description')}

+ +

+ + {t('navigateToAbout')} + +

+
+ ); +} diff --git a/examples/example-next-13-with-pages/src/components/LocaleSwitcher.tsx b/examples/example-next-13-with-pages/src/components/LocaleSwitcher.tsx new file mode 100644 index 000000000..f01811ff9 --- /dev/null +++ b/examples/example-next-13-with-pages/src/components/LocaleSwitcher.tsx @@ -0,0 +1,14 @@ +import Link from 'next/link'; +import {useLocale, useTranslations} from 'next-intl'; + +export default function LocaleSwitcher() { + const t = useTranslations('LocaleSwitcher'); + const locale = useLocale(); + const otherLocale = locale === 'en' ? 'de' : 'en'; + + return ( + + {t('switchLocale', {locale: otherLocale})} + + ); +} diff --git a/examples/example-next-13-with-pages/src/components/PageLayout.tsx b/examples/example-next-13-with-pages/src/components/PageLayout.tsx new file mode 100644 index 000000000..bc840a0cb --- /dev/null +++ b/examples/example-next-13-with-pages/src/components/PageLayout.tsx @@ -0,0 +1,25 @@ +import {ReactNode} from 'react'; + +type Props = { + children?: ReactNode; + title: string; +}; + +export default function PageLayout({children, title}: Props) { + return ( + <> +
+
+

{title}

+ {children} +
+
+ + ); +} diff --git a/examples/example-next-13-with-pages/src/i18n.ts b/examples/example-next-13-with-pages/src/i18n.ts new file mode 100644 index 000000000..c45ddee14 --- /dev/null +++ b/examples/example-next-13-with-pages/src/i18n.ts @@ -0,0 +1,5 @@ +import {getRequestConfig} from 'next-intl/server'; + +export default getRequestConfig(async ({locale}) => ({ + messages: (await import(`../messages/${locale}.json`)).default +})); diff --git a/examples/example-next-13/src/middleware.tsx b/examples/example-next-13-with-pages/src/middleware.ts similarity index 83% rename from examples/example-next-13/src/middleware.tsx rename to examples/example-next-13-with-pages/src/middleware.ts index 8675074bb..76a4688cc 100644 --- a/examples/example-next-13/src/middleware.tsx +++ 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/[locale]/about.tsx b/examples/example-next-13-with-pages/src/pages/[locale]/about.tsx new file mode 100644 index 000000000..5814df570 --- /dev/null +++ b/examples/example-next-13-with-pages/src/pages/[locale]/about.tsx @@ -0,0 +1,29 @@ +import {GetStaticPropsContext} from 'next'; +import {useTranslations} from 'next-intl'; +import PageLayout from '../../components/PageLayout'; + +export default function About() { + const t = useTranslations('About'); + + return ( + +

{t('description')}

+
+ ); +} + +export async function getStaticProps({params}: GetStaticPropsContext) { + return { + props: { + messages: (await import(`../../../messages/${params?.locale}.json`)) + .default + } + }; +} + +export async function getStaticPaths() { + return { + paths: [{params: {locale: 'de'}}, {params: {locale: 'en'}}], + fallback: false + }; +} diff --git a/examples/example-next-13-with-pages/src/pages/_app.tsx b/examples/example-next-13-with-pages/src/pages/_app.tsx new file mode 100644 index 000000000..707e0ec8b --- /dev/null +++ b/examples/example-next-13-with-pages/src/pages/_app.tsx @@ -0,0 +1,21 @@ +import {AppProps} from 'next/app'; +import {NextRouter, withRouter} from 'next/router'; +import {NextIntlClientProvider} from 'next-intl'; + +type Props = AppProps & { + router: NextRouter; +}; + +function App({Component, pageProps, router}: Props) { + return ( + + + + ); +} + +export default withRouter(App); diff --git a/examples/example-next-13-with-pages/tsconfig.json b/examples/example-next-13-with-pages/tsconfig.json new file mode 100644 index 000000000..fbc267007 --- /dev/null +++ b/examples/example-next-13-with-pages/tsconfig.json @@ -0,0 +1,36 @@ +{ + "extends": "eslint-config-molindo/tsconfig.json", + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "strictNullChecks": true + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/examples/example-next-13-with-pages/yarn.lock b/examples/example-next-13-with-pages/yarn.lock new file mode 100644 index 000000000..fe8578ba7 --- /dev/null +++ b/examples/example-next-13-with-pages/yarn.lock @@ -0,0 +1,2409 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/helper-validator-identifier@^7.15.7", "@babel/helper-validator-identifier@^7.18.6": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/runtime-corejs3@^7.10.2": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.20.1.tgz#d0775a49bb5fba77e42cbb7276c9955c7b05af8d" + integrity sha512-CGulbEDcg/ND1Im7fUNRZdGXmX2MTWVVZacQi/6DiKE5HNwZ3aVTm5PV4lO8HHz0B2h8WQyvKKjbX5XgTtydsg== + dependencies: + core-js-pure "^3.25.1" + regenerator-runtime "^0.13.10" + +"@babel/runtime@^7.10.2", "@babel/runtime@^7.18.9": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9" + integrity sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg== + dependencies: + regenerator-runtime "^0.13.10" + +"@eslint/eslintrc@^1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95" + integrity sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.4.0" + globals "^13.15.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@formatjs/ecma402-abstract@1.11.4": + version "1.11.4" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz#b962dfc4ae84361f9f08fbce411b4e4340930eda" + integrity sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw== + dependencies: + "@formatjs/intl-localematcher" "0.2.25" + tslib "^2.1.0" + +"@formatjs/ecma402-abstract@^1.11.4": + version "1.13.0" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.13.0.tgz#df6db3cbee0182bbd2fd6217103781c802aee819" + integrity sha512-CQ8Ykd51jYD1n05dtoX6ns6B9n/+6ZAxnWUAonvHC4kkuAemROYBhHkEB4tm1uVrRlE7gLDqXkAnY51Y0pRCWQ== + dependencies: + "@formatjs/intl-localematcher" "0.2.31" + tslib "2.4.0" + +"@formatjs/fast-memoize@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz#e6f5aee2e4fd0ca5edba6eba7668e2d855e0fc21" + integrity sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg== + dependencies: + tslib "^2.1.0" + +"@formatjs/icu-messageformat-parser@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz#a54293dd7f098d6a6f6a084ab08b6d54a3e8c12d" + integrity sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw== + dependencies: + "@formatjs/ecma402-abstract" "1.11.4" + "@formatjs/icu-skeleton-parser" "1.3.6" + tslib "^2.1.0" + +"@formatjs/icu-skeleton-parser@1.3.6": + version "1.3.6" + resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz#4ce8c0737d6f07b735288177049e97acbf2e8964" + integrity sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg== + dependencies: + "@formatjs/ecma402-abstract" "1.11.4" + tslib "^2.1.0" + +"@formatjs/intl-localematcher@0.2.25": + version "0.2.25" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz#60892fe1b271ec35ba07a2eb018a2dd7bca6ea3a" + integrity sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA== + dependencies: + tslib "^2.1.0" + +"@formatjs/intl-localematcher@0.2.31": + version "0.2.31" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.31.tgz#aada2b1e58211460cedba56889e3c489117eb6eb" + integrity sha512-9QTjdSBpQ7wHShZgsNzNig5qT3rCPvmZogS/wXZzKotns5skbXgs0I7J8cuN0PPqXyynvNVuN+iOKhNS2eb+ZA== + dependencies: + tslib "2.4.0" + +"@humanwhocodes/config-array@^0.11.6": + version "0.11.7" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.7.tgz#38aec044c6c828f6ed51d5d7ae3d9b9faf6dbb0f" + integrity sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@next/env@13.1.6": + version "13.1.6" + resolved "https://registry.yarnpkg.com/@next/env/-/env-13.1.6.tgz#c4925609f16142ded1a5cb833359ab17359b7a93" + integrity sha512-s+W9Fdqh5MFk6ECrbnVmmAOwxKQuhGMT7xXHrkYIBMBcTiOqNWhv5KbJIboKR5STXxNXl32hllnvKaffzFaWQg== + +"@next/eslint-plugin-next@13.0.4": + version "13.0.4" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.0.4.tgz#46800f48d9f81e8bb460319a15a00bf2f4016e91" + integrity sha512-jZ4urKT+aO9QHm3ttihrIQscQISDSKK8isAom750+EySn9o3LCSkTdPGBtvBqY7Yku+NqhfQempR5J58DqTaVg== + dependencies: + glob "7.1.7" + +"@next/swc-android-arm-eabi@13.1.6": + version "13.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.1.6.tgz#d766dfc10e27814d947b20f052067c239913dbcc" + integrity sha512-F3/6Z8LH/pGlPzR1AcjPFxx35mPqjE5xZcf+IL+KgbW9tMkp7CYi1y7qKrEWU7W4AumxX/8OINnDQWLiwLasLQ== + +"@next/swc-android-arm64@13.1.6": + version "13.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-13.1.6.tgz#f37a98d5f18927d8c9970d750d516ac779465176" + integrity sha512-cMwQjnB8vrYkWyK/H0Rf2c2pKIH4RGjpKUDvbjVAit6SbwPDpmaijLio0LWFV3/tOnY6kvzbL62lndVA0mkYpw== + +"@next/swc-darwin-arm64@13.1.6": + version "13.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.1.6.tgz#ec1b90fd9bf809d8b81004c5182e254dced4ad96" + integrity sha512-KKRQH4DDE4kONXCvFMNBZGDb499Hs+xcFAwvj+rfSUssIDrZOlyfJNy55rH5t2Qxed1e4K80KEJgsxKQN1/fyw== + +"@next/swc-darwin-x64@13.1.6": + version "13.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.1.6.tgz#e869ac75d16995eee733a7d1550322d9051c1eb4" + integrity sha512-/uOky5PaZDoaU99ohjtNcDTJ6ks/gZ5ykTQDvNZDjIoCxFe3+t06bxsTPY6tAO6uEAw5f6vVFX5H5KLwhrkZCA== + +"@next/swc-freebsd-x64@13.1.6": + version "13.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.1.6.tgz#84a7b2e423a2904afc2edca21c2f1ba6b53fa4c1" + integrity sha512-qaEALZeV7to6weSXk3Br80wtFQ7cFTpos/q+m9XVRFggu+8Ib895XhMWdJBzew6aaOcMvYR6KQ6JmHA2/eMzWw== + +"@next/swc-linux-arm-gnueabihf@13.1.6": + version "13.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.1.6.tgz#980eed1f655ff8a72187d8a6ef9e73ac39d20d23" + integrity sha512-OybkbC58A1wJ+JrJSOjGDvZzrVEQA4sprJejGqMwiZyLqhr9Eo8FXF0y6HL+m1CPCpPhXEHz/2xKoYsl16kNqw== + +"@next/swc-linux-arm64-gnu@13.1.6": + version "13.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.1.6.tgz#87a71db21cded3f7c63d1d19079845c59813c53d" + integrity sha512-yCH+yDr7/4FDuWv6+GiYrPI9kcTAO3y48UmaIbrKy8ZJpi7RehJe3vIBRUmLrLaNDH3rY1rwoHi471NvR5J5NQ== + +"@next/swc-linux-arm64-musl@13.1.6": + version "13.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.1.6.tgz#c5aac8619331b9fd030603bbe2b36052011e11de" + integrity sha512-ECagB8LGX25P9Mrmlc7Q/TQBb9rGScxHbv/kLqqIWs2fIXy6Y/EiBBiM72NTwuXUFCNrWR4sjUPSooVBJJ3ESQ== + +"@next/swc-linux-x64-gnu@13.1.6": + version "13.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.1.6.tgz#9513d36d540bbfea575576746736054c31aacdea" + integrity sha512-GT5w2mruk90V/I5g6ScuueE7fqj/d8Bui2qxdw6lFxmuTgMeol5rnzAv4uAoVQgClOUO/MULilzlODg9Ib3Y4Q== + +"@next/swc-linux-x64-musl@13.1.6": + version "13.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.1.6.tgz#d61fc6884899f5957251f4ce3f522e34a2c479b7" + integrity sha512-keFD6KvwOPzmat4TCnlnuxJCQepPN+8j3Nw876FtULxo8005Y9Ghcl7ACcR8GoiKoddAq8gxNBrpjoxjQRHeAQ== + +"@next/swc-win32-arm64-msvc@13.1.6": + version "13.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.1.6.tgz#fac2077a8ae9768e31444c9ae90807e64117cda7" + integrity sha512-OwertslIiGQluFvHyRDzBCIB07qJjqabAmINlXUYt7/sY7Q7QPE8xVi5beBxX/rxTGPIbtyIe3faBE6Z2KywhQ== + +"@next/swc-win32-ia32-msvc@13.1.6": + version "13.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.1.6.tgz#498bc11c91b4c482a625bf4b978f98ae91111e46" + integrity sha512-g8zowiuP8FxUR9zslPmlju7qYbs2XBtTLVSxVikPtUDQedhcls39uKYLvOOd1JZg0ehyhopobRoH1q+MHlIN/w== + +"@next/swc-win32-x64-msvc@13.1.6": + version "13.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.1.6.tgz#17ed919c723426b7d0ce1cd73d40ce3dcd342089" + integrity sha512-Ls2OL9hi3YlJKGNdKv8k3X/lLgc3VmLG3a/DeTkAd+lAituJp8ZHmRmm9f9SL84fT3CotlzcgbdaCDfFwFA6bA== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@pkgr/utils@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.3.1.tgz#0a9b06ffddee364d6642b3cd562ca76f55b34a03" + integrity sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw== + dependencies: + cross-spawn "^7.0.3" + is-glob "^4.0.3" + open "^8.4.0" + picocolors "^1.0.0" + tiny-glob "^0.2.9" + tslib "^2.4.0" + +"@rushstack/eslint-patch@^1.1.1", "@rushstack/eslint-patch@^1.1.3": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728" + integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== + +"@swc/helpers@0.4.14": + version "0.4.14" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.14.tgz#1352ac6d95e3617ccb7c1498ff019654f1e12a74" + integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw== + dependencies: + tslib "^2.4.0" + +"@types/json-schema@^7.0.9": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + +"@types/lodash@^4.14.176": + version "4.14.189" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.189.tgz#975ff8c38da5ae58b751127b19ad5e44b5b7f6d2" + integrity sha512-kb9/98N6X8gyME9Cf7YaqIMvYGnBSWqEci6tiettE6iJWH1XdJz/PO8LB0GtLCG7x8dU3KWhZT+lA1a35127tA== + +"@types/node@^17.0.23": + version "17.0.45" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" + integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== + +"@types/normalize-package-data@^2.4.0": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" + integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== + +"@types/prop-types@*": + version "15.7.5" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + +"@types/react@^18.0.23": + version "18.0.25" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.25.tgz#8b1dcd7e56fe7315535a4af25435e0bb55c8ae44" + integrity sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + +"@types/semver@^7.3.12": + version "7.3.13" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" + integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== + +"@typescript-eslint/eslint-plugin@^5.0.0": + version "5.44.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.44.0.tgz#105788f299050c917eb85c4d9fd04b089e3740de" + integrity sha512-j5ULd7FmmekcyWeArx+i8x7sdRHzAtXTkmDPthE4amxZOWKFK7bomoJ4r7PJ8K7PoMzD16U8MmuZFAonr1ERvw== + dependencies: + "@typescript-eslint/scope-manager" "5.44.0" + "@typescript-eslint/type-utils" "5.44.0" + "@typescript-eslint/utils" "5.44.0" + debug "^4.3.4" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + regexpp "^3.2.0" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@^5.42.0": + version "5.44.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.44.0.tgz#99e2c710a2252191e7a79113264f438338b846ad" + integrity sha512-H7LCqbZnKqkkgQHaKLGC6KUjt3pjJDx8ETDqmwncyb6PuoigYajyAwBGz08VU/l86dZWZgI4zm5k2VaKqayYyA== + dependencies: + "@typescript-eslint/scope-manager" "5.44.0" + "@typescript-eslint/types" "5.44.0" + "@typescript-eslint/typescript-estree" "5.44.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.44.0": + version "5.44.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.44.0.tgz#988c3f34b45b3474eb9ff0674c18309dedfc3e04" + integrity sha512-2pKml57KusI0LAhgLKae9kwWeITZ7IsZs77YxyNyIVOwQ1kToyXRaJLl+uDEXzMN5hnobKUOo2gKntK9H1YL8g== + dependencies: + "@typescript-eslint/types" "5.44.0" + "@typescript-eslint/visitor-keys" "5.44.0" + +"@typescript-eslint/type-utils@5.44.0": + version "5.44.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.44.0.tgz#bc5a6e8a0269850714a870c9268c038150dfb3c7" + integrity sha512-A1u0Yo5wZxkXPQ7/noGkRhV4J9opcymcr31XQtOzcc5nO/IHN2E2TPMECKWYpM3e6olWEM63fq/BaL1wEYnt/w== + dependencies: + "@typescript-eslint/typescript-estree" "5.44.0" + "@typescript-eslint/utils" "5.44.0" + debug "^4.3.4" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.44.0": + version "5.44.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.44.0.tgz#f3f0b89aaff78f097a2927fe5688c07e786a0241" + integrity sha512-Tp+zDnHmGk4qKR1l+Y1rBvpjpm5tGXX339eAlRBDg+kgZkz9Bw+pqi4dyseOZMsGuSH69fYfPJCBKBrbPCxYFQ== + +"@typescript-eslint/typescript-estree@5.44.0": + version "5.44.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.44.0.tgz#0461b386203e8d383bb1268b1ed1da9bc905b045" + integrity sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw== + dependencies: + "@typescript-eslint/types" "5.44.0" + "@typescript-eslint/visitor-keys" "5.44.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.44.0", "@typescript-eslint/utils@^5.10.0": + version "5.44.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.44.0.tgz#d733da4d79d6c30f1a68b531cdda1e0c1f00d52d" + integrity sha512-fMzA8LLQ189gaBjS0MZszw5HBdZgVwxVFShCO3QN+ws3GlPkcy9YuS3U4wkT6su0w+Byjq3mS3uamy9HE4Yfjw== + dependencies: + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.44.0" + "@typescript-eslint/types" "5.44.0" + "@typescript-eslint/typescript-estree" "5.44.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + semver "^7.3.7" + +"@typescript-eslint/visitor-keys@5.44.0": + version "5.44.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.44.0.tgz#10740dc28902bb903d12ee3a005cc3a70207d433" + integrity sha512-a48tLG8/4m62gPFbJ27FxwCOqPKxsb8KC3HkmYoq2As/4YyjQl1jDbRr1s63+g4FS/iIehjmN3L5UjmKva1HzQ== + dependencies: + "@typescript-eslint/types" "5.44.0" + eslint-visitor-keys "^3.3.0" + +accept-language-parser@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/accept-language-parser/-/accept-language-parser-1.5.0.tgz#8877c54040a8dcb59e0a07d9c1fde42298334791" + integrity sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw== + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.8.0: + version "8.8.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" + integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== + +ajv@^6.10.0, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +aria-query@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" + integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== + dependencies: + "@babel/runtime" "^7.10.2" + "@babel/runtime-corejs3" "^7.10.2" + +array-includes@^3.1.4, array-includes@^3.1.5, array-includes@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f" + integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + get-intrinsic "^1.1.3" + is-string "^1.0.7" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.flat@^1.2.5: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" + integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183" + integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + +array.prototype.tosorted@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz#ccf44738aa2b5ac56578ffda97c03fd3e23dd532" + integrity sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + get-intrinsic "^1.1.3" + +ast-types-flow@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" + integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== + +axe-core@^4.4.3: + version "4.5.2" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.5.2.tgz#823fdf491ff717ac3c58a52631d4206930c1d9f7" + integrity sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA== + +axobject-query@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" + integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +builtin-modules@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +caniuse-lite@^1.0.30001406: + version "1.0.30001434" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz#ec1ec1cfb0a93a34a0600d37903853030520a4e5" + integrity sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +ci-info@^3.3.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.6.2.tgz#362ea15378f1c39378ba786affbc1c9ef015ecfd" + integrity sha512-lVZdhvbEudris15CLytp2u6Y0p5EKfztae9Fqa189MfNmln9F33XuH69v5fvNfiRN5/0eAUz2yJL3mo+nhaRKg== + +clean-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clean-regexp/-/clean-regexp-1.0.0.tgz#8df7c7aae51fd36874e8f8d05b9180bc11a3fed7" + integrity sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw== + dependencies: + escape-string-regexp "^1.0.5" + +client-only@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +confusing-browser-globals@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" + integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== + +core-js-pure@^3.25.1: + version "3.26.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.26.1.tgz#653f4d7130c427820dcecd3168b594e8bb095a33" + integrity sha512-VVXcDpp/xJ21KdULRq/lXdLzQAtX7+37LzpyfFM973il0tWSsDEoyzG38G14AjTpK9VTfiNM9jnFauq/CpaWGQ== + +cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +csstype@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" + integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== + +damerau-levenshtein@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" + integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== + +debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +define-properties@^1.1.3, define-properties@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +enhanced-resolve@^5.10.0: + version "5.11.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.11.0.tgz#543cf6c847a85adba0c4a5e2170bded4d493919a" + integrity sha512-0Gcraf7gAJSQoPg+bTSXNhuzAYtXqLc4C011vb8S3B8XUSEkGYNBk20c68X9291VF4vvsCD8SPkr6Mza+DwU+g== + dependencies: + graceful-fs "^4.2.9" + tapable "^2.2.0" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.19.0, es-abstract@^1.20.4: + version "1.20.4" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.4.tgz#1d103f9f8d78d4cf0713edcd6d0ed1a46eed5861" + integrity sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.3" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-weakref "^1.0.2" + object-inspect "^1.12.2" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + safe-regex-test "^1.0.0" + string.prototype.trimend "^1.0.5" + string.prototype.trimstart "^1.0.5" + unbox-primitive "^1.0.2" + +es-shim-unscopables@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-molindo@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/eslint-config-molindo/-/eslint-config-molindo-6.0.0.tgz#2a5ac0acd5e43f3428c60fb0b51881bc5ba42ae4" + integrity sha512-kZ/SZBJIejZY45HVn9q0WSkF1mhvem+7FmxDF7mNfCepGEGrjID2mNlOuWAShBTidzpukMQ7qKhkjV5JVmO91w== + dependencies: + "@rushstack/eslint-patch" "^1.1.1" + "@typescript-eslint/eslint-plugin" "^5.0.0" + "@typescript-eslint/parser" "^5.0.0" + confusing-browser-globals "^1.0.11" + eslint-plugin-css-modules "^2.11.0" + eslint-plugin-import "^2.25.4" + eslint-plugin-jest "^26.1.3" + eslint-plugin-jsx-a11y "^6.5.1" + eslint-plugin-prettier "^4.0.0" + eslint-plugin-react "^7.29.4" + eslint-plugin-react-hooks "^4.3.0" + eslint-plugin-sort-destructure-keys "^1.4.0" + eslint-plugin-unicorn "^41.0.1" + prettier "^2.6.1" + typescript "^4.0.0" + +eslint-config-next@^13.0.0: + version "13.0.4" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.0.4.tgz#78954caf26b3aa718bed077bce90c1f505db0005" + integrity sha512-moEC7BW2TK7JKq3QfnaauqRjWzVcEf71gp5DbClpFPHM6QXE0u0uVvSTiHlmOgtCe1vyWAO+AhF87ZITd8mIDw== + dependencies: + "@next/eslint-plugin-next" "13.0.4" + "@rushstack/eslint-patch" "^1.1.3" + "@typescript-eslint/parser" "^5.42.0" + eslint-import-resolver-node "^0.3.6" + eslint-import-resolver-typescript "^3.5.2" + eslint-plugin-import "^2.26.0" + eslint-plugin-jsx-a11y "^6.5.1" + eslint-plugin-react "^7.31.7" + eslint-plugin-react-hooks "^4.5.0" + +eslint-import-resolver-node@^0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" + integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== + dependencies: + debug "^3.2.7" + resolve "^1.20.0" + +eslint-import-resolver-typescript@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.2.tgz#9431acded7d898fd94591a08ea9eec3514c7de91" + integrity sha512-zX4ebnnyXiykjhcBvKIf5TNvt8K7yX6bllTRZ14MiurKPjDpCAZujlszTdB8pcNXhZcOf+god4s9SjQa5GnytQ== + dependencies: + debug "^4.3.4" + enhanced-resolve "^5.10.0" + get-tsconfig "^4.2.0" + globby "^13.1.2" + is-core-module "^2.10.0" + is-glob "^4.0.3" + synckit "^0.8.4" + +eslint-module-utils@^2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974" + integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== + dependencies: + debug "^3.2.7" + +eslint-plugin-css-modules@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-css-modules/-/eslint-plugin-css-modules-2.11.0.tgz#8de4d01d523a2d51c03043fa8004aab6b6cf3b1a" + integrity sha512-CLvQvJOMlCywZzaI4HVu7QH/ltgNXvCg7giJGiE+sA9wh5zQ+AqTgftAzrERV22wHe1p688wrU/Zwxt1Ry922w== + dependencies: + gonzales-pe "^4.0.3" + lodash "^4.17.2" + +eslint-plugin-import@^2.25.4, eslint-plugin-import@^2.26.0: + version "2.26.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b" + integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA== + dependencies: + array-includes "^3.1.4" + array.prototype.flat "^1.2.5" + debug "^2.6.9" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.6" + eslint-module-utils "^2.7.3" + has "^1.0.3" + is-core-module "^2.8.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.values "^1.1.5" + resolve "^1.22.0" + tsconfig-paths "^3.14.1" + +eslint-plugin-jest@^26.1.3: + version "26.9.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-26.9.0.tgz#7931c31000b1c19e57dbfb71bbf71b817d1bf949" + integrity sha512-TWJxWGp1J628gxh2KhaH1H1paEdgE2J61BBF1I59c6xWeL5+D1BzMxGDN/nXAfX+aSkR5u80K+XhskK6Gwq9ng== + dependencies: + "@typescript-eslint/utils" "^5.10.0" + +eslint-plugin-jsx-a11y@^6.5.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz#93736fc91b83fdc38cc8d115deedfc3091aef1ff" + integrity sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q== + dependencies: + "@babel/runtime" "^7.18.9" + aria-query "^4.2.2" + array-includes "^3.1.5" + ast-types-flow "^0.0.7" + axe-core "^4.4.3" + axobject-query "^2.2.0" + damerau-levenshtein "^1.0.8" + emoji-regex "^9.2.2" + has "^1.0.3" + jsx-ast-utils "^3.3.2" + language-tags "^1.0.5" + minimatch "^3.1.2" + semver "^6.3.0" + +eslint-plugin-prettier@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b" + integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ== + dependencies: + prettier-linter-helpers "^1.0.0" + +eslint-plugin-react-hooks@^4.3.0, eslint-plugin-react-hooks@^4.5.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" + integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== + +eslint-plugin-react@^7.29.4, eslint-plugin-react@^7.31.7: + version "7.31.11" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.31.11.tgz#011521d2b16dcf95795df688a4770b4eaab364c8" + integrity sha512-TTvq5JsT5v56wPa9OYHzsrOlHzKZKjV+aLgS+55NJP/cuzdiQPC7PfYoUjMoxlffKtvijpk7vA/jmuqRb9nohw== + dependencies: + array-includes "^3.1.6" + array.prototype.flatmap "^1.3.1" + array.prototype.tosorted "^1.1.1" + doctrine "^2.1.0" + estraverse "^5.3.0" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.6" + object.fromentries "^2.0.6" + object.hasown "^1.1.2" + object.values "^1.1.6" + prop-types "^15.8.1" + resolve "^2.0.0-next.3" + semver "^6.3.0" + string.prototype.matchall "^4.0.8" + +eslint-plugin-sort-destructure-keys@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-sort-destructure-keys/-/eslint-plugin-sort-destructure-keys-1.4.0.tgz#45924521e556692002522468a75b6a9fbac11316" + integrity sha512-txU9l22mblz7YpyjJNYFy4wb5PVXiRMbc9lqFPPhvY4wKyBBYQvb31TIcduf7iRb4Bv01aiXcJiuCkOOrVY48Q== + dependencies: + natural-compare-lite "^1.4.0" + +eslint-plugin-unicorn@^41.0.1: + version "41.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-41.0.1.tgz#b49205b38e71e227d21fb5776f8d078a1dc637ca" + integrity sha512-gF5vo2dIj0YdNMQ/IMegiBkQdQ22GBFFVpdkJP+0og3w7XD4ypea0xQVRv6iofkLVR2w0phAdikcnU01ybd4Ow== + dependencies: + "@babel/helper-validator-identifier" "^7.15.7" + ci-info "^3.3.0" + clean-regexp "^1.0.0" + eslint-utils "^3.0.0" + esquery "^1.4.0" + indent-string "^4.0.0" + is-builtin-module "^3.1.0" + lodash "^4.17.21" + pluralize "^8.0.0" + read-pkg-up "^7.0.1" + regexp-tree "^0.1.24" + safe-regex "^2.1.1" + semver "^7.3.5" + strip-indent "^3.0.0" + +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@^8.12.0: + version "8.28.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.28.0.tgz#81a680732634677cc890134bcdd9fdfea8e63d6e" + integrity sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ== + dependencies: + "@eslint/eslintrc" "^1.3.3" + "@humanwhocodes/config-array" "^0.11.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.4.0" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.15.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-sdsl "^4.1.4" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + +espree@^9.4.0: + version "9.4.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.1.tgz#51d6092615567a2c2cff7833445e37c28c0065bd" + integrity sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg== + dependencies: + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.0" + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-diff@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" + integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== + +fast-glob@^3.2.11, fast-glob@^3.2.9: + version "3.2.12" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +get-tsconfig@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.2.0.tgz#ff368dd7104dab47bf923404eb93838245c66543" + integrity sha512-X8u8fREiYOE6S8hLbq99PeykTDoLVnxvF4DjWKJmz9xy2nNRdUcV8ZN9tniJFeKyTU3qnC9lL8n4Chd6LmVKHg== + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@7.1.7: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^13.15.0: + version "13.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.18.0.tgz#fb224daeeb2bb7d254cd2c640f003528b8d0c1dc" + integrity sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A== + dependencies: + type-fest "^0.20.2" + +globalyzer@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" + integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +globby@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.2.tgz#29047105582427ab6eca4f905200667b056da515" + integrity sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ== + dependencies: + dir-glob "^3.0.1" + fast-glob "^3.2.11" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^4.0.0" + +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + +gonzales-pe@^4.0.3: + version "4.3.0" + resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3" + integrity sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ== + dependencies: + minimist "^1.2.5" + +graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + +has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hosted-git-info@^2.1.4: + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + +ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + +intl-messageformat@^9.3.18: + version "9.13.0" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-9.13.0.tgz#97360b73bd82212e4f6005c712a4a16053165468" + integrity sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw== + dependencies: + "@formatjs/ecma402-abstract" "1.11.4" + "@formatjs/fast-memoize" "1.2.1" + "@formatjs/icu-messageformat-parser" "2.1.0" + tslib "^2.1.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-builtin-module@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.0.tgz#bb0310dfe881f144ca83f30100ceb10cf58835e0" + integrity sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw== + dependencies: + builtin-modules "^3.3.0" + +is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-core-module@^2.10.0, is-core-module@^2.8.1, is-core-module@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" + integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== + dependencies: + has "^1.0.3" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +js-sdsl@^4.1.4: + version "4.2.0" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0" + integrity sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea" + integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw== + dependencies: + array-includes "^3.1.5" + object.assign "^4.1.3" + +language-subtag-registry@~0.3.2: + version "0.3.22" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" + integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== + +language-tags@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a" + integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ== + dependencies: + language-subtag-registry "~0.3.2" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash@^4.17.2, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" + integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== + +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +next-intl@^2.10.2: + version "2.10.2" + resolved "https://registry.yarnpkg.com/next-intl/-/next-intl-2.10.2.tgz#ea7d49902acb33ea5e6e351c92be3db0da713680" + integrity sha512-wMNNn8l/fwKprYhH6rLQb68GEBkZaACBl99tELL/Tw+AWkGKfvEmnfYEdkiqm5OBQbaJn+gH9gMzGMsJzs1MLA== + dependencies: + accept-language-parser "^1.5.0" + use-intl "^2.10.2" + +next@^13.1.1: + version "13.1.6" + resolved "https://registry.yarnpkg.com/next/-/next-13.1.6.tgz#054babe20b601f21f682f197063c9b0b32f1a27c" + integrity sha512-hHlbhKPj9pW+Cymvfzc15lvhaOZ54l+8sXDXJWm3OBNBzgrVj6hwGPmqqsXg40xO1Leq+kXpllzRPuncpC0Phw== + dependencies: + "@next/env" "13.1.6" + "@swc/helpers" "0.4.14" + caniuse-lite "^1.0.30001406" + postcss "8.4.14" + styled-jsx "5.1.1" + optionalDependencies: + "@next/swc-android-arm-eabi" "13.1.6" + "@next/swc-android-arm64" "13.1.6" + "@next/swc-darwin-arm64" "13.1.6" + "@next/swc-darwin-x64" "13.1.6" + "@next/swc-freebsd-x64" "13.1.6" + "@next/swc-linux-arm-gnueabihf" "13.1.6" + "@next/swc-linux-arm64-gnu" "13.1.6" + "@next/swc-linux-arm64-musl" "13.1.6" + "@next/swc-linux-x64-gnu" "13.1.6" + "@next/swc-linux-x64-musl" "13.1.6" + "@next/swc-win32-arm64-msvc" "13.1.6" + "@next/swc-win32-ia32-msvc" "13.1.6" + "@next/swc-win32-x64-msvc" "13.1.6" + +normalize-package-data@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.12.2, object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.3, object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.entries@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.6.tgz#9737d0e5b8291edd340a3e3264bb8a3b00d5fa23" + integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +object.fromentries@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73" + integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +object.hasown@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.2.tgz#f919e21fad4eb38a57bc6345b3afd496515c3f92" + integrity sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw== + dependencies: + define-properties "^1.1.4" + es-abstract "^1.20.4" + +object.values@^1.1.5, object.values@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" + integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +open@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" + integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + +postcss@8.4.14: + version "8.4.14" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" + integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@^2.6.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" + integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== + +prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" + +regenerator-runtime@^0.13.10: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + +regexp-tree@^0.1.24, regexp-tree@~0.1.1: + version "0.1.24" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.24.tgz#3d6fa238450a4d66e5bc9c4c14bb720e2196829d" + integrity sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw== + +regexp.prototype.flags@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + functions-have-names "^1.2.2" + +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.10.0, resolve@^1.20.0, resolve@^1.22.0: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^2.0.0-next.3: + version "2.0.0-next.4" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660" + integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + +safe-regex@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-2.1.1.tgz#f7128f00d056e2fe5c11e81a1324dd974aadced2" + integrity sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A== + dependencies: + regexp-tree "~0.1.1" + +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + +"semver@2 || 3 || 4 || 5": + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.3.5, semver@^7.3.7: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +spdx-correct@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.12" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz#69077835abe2710b65f03969898b6637b505a779" + integrity sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA== + +string.prototype.matchall@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3" + integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + regexp.prototype.flags "^1.4.3" + side-channel "^1.0.4" + +string.prototype.trimend@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" + integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +string.prototype.trimstart@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" + integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +styled-jsx@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" + integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== + dependencies: + client-only "0.0.1" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +synckit@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.4.tgz#0e6b392b73fafdafcde56692e3352500261d64ec" + integrity sha512-Dn2ZkzMdSX827QbowGbU/4yjWuvNaCoScLLoMo/yKbu+P4GBR6cRGKZH27k6a9bRzdqcyd1DE96pQtQ6uNkmyw== + dependencies: + "@pkgr/utils" "^2.3.1" + tslib "^2.4.0" + +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +tiny-glob@^0.2.9: + version "0.2.9" + resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" + integrity sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg== + dependencies: + globalyzer "0.1.0" + globrex "^0.1.2" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +tsconfig-paths@^3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.1.0, tslib@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" + integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +typescript@^4.0.0, typescript@^4.6.3: + version "4.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.3.tgz#3aea307c1746b8c384435d8ac36b8a2e580d85db" + integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA== + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +use-intl@^2.10.2: + version "2.10.2" + resolved "https://registry.yarnpkg.com/use-intl/-/use-intl-2.10.2.tgz#100648eec697a6950a164eeb1df8d243cf4a7ce2" + integrity sha512-4RT+Vc8Dkzw+I2e4DEa+hjLwZow5sj7nqJYVr+UttZ5LVkmOP6IuVMgEhnjHHperL+ywlA5nhrOTTetTVzMpBg== + dependencies: + "@formatjs/ecma402-abstract" "^1.11.4" + intl-messageformat "^9.3.18" + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/examples/example-next-13/.eslintrc.js b/examples/example-next-13/.eslintrc.js index c6d534ab4..4bbd03f7b 100644 --- a/examples/example-next-13/.eslintrc.js +++ b/examples/example-next-13/.eslintrc.js @@ -1,4 +1,7 @@ module.exports = { + env: { + node: true + }, extends: [ 'molindo/typescript', 'molindo/react', diff --git a/examples/example-next-13/jest.config.js b/examples/example-next-13/jest.config.js index 9c3b5fe4b..e968d798b 100644 --- a/examples/example-next-13/jest.config.js +++ b/examples/example-next-13/jest.config.js @@ -1,8 +1,13 @@ +/* eslint-env node */ const nextJest = require('next/jest'); const createJestConfig = nextJest({dir: './'}); module.exports = createJestConfig({ + moduleNameMapper: { + '^next$': require.resolve('next'), + '^next/navigation$': require.resolve('next/navigation') + }, testEnvironment: 'jsdom', rootDir: 'src' }); diff --git a/examples/example-next-13/messages/de.json b/examples/example-next-13/messages/de.json index bc81a392f..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 (/de/about).

" + "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 d00fc0394..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 (/de/about).

" + "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/next.config.js b/examples/example-next-13/next.config.js new file mode 100644 index 000000000..c52d03364 --- /dev/null +++ b/examples/example-next-13/next.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const withNextIntl = require('next-intl/plugin')(); + +module.exports = withNextIntl(); diff --git a/examples/example-next-13/package.json b/examples/example-next-13/package.json index 1b74888ac..1ab65a269 100644 --- a/examples/example-next-13/package.json +++ b/examples/example-next-13/package.json @@ -21,12 +21,12 @@ }, "devDependencies": { "@jest/globals": "^29.5.0", - "@playwright/test": "^1.28.1", + "@playwright/test": "^1.33.0", "@testing-library/react": "^13.0.0", "@types/jest": "^29.5.0", "@types/lodash": "^4.14.176", "@types/node": "^17.0.23", - "@types/react": "^18.2.5", + "@types/react": "^18.2.29", "autoprefixer": "^10.4.0", "eslint": "^8.46.0", "eslint-config-molindo": "^7.0.0", @@ -35,6 +35,6 @@ "jest-environment-jsdom": "^29.5.0", "postcss": "^8.4.23", "prettier-plugin-tailwindcss": "^0.2.3", - "typescript": "^5.0.0" + "typescript": "^5.2.2" } } diff --git a/examples/example-next-13/playwright.config.ts b/examples/example-next-13/playwright.config.ts index 368c3adbb..2807e6b4b 100644 --- a/examples/example-next-13/playwright.config.ts +++ b/examples/example-next-13/playwright.config.ts @@ -6,6 +6,7 @@ import {devices} from '@playwright/test'; const PORT = process.env.CI ? 3001 : 3000; const config: PlaywrightTestConfig = { + retries: process.env.CI ? 1 : 0, testDir: './tests', projects: [ { 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 1dcd58823..1d31c44a7 100644 --- a/examples/example-next-13/src/app/[locale]/about/page.tsx +++ b/examples/example-next-13/src/app/[locale]/about/page.tsx @@ -1,14 +1,20 @@ -'use client'; - import {useTranslations} from 'next-intl'; +import {unstable_setRequestLocale} from 'next-intl/server'; import PageLayout from 'components/PageLayout'; -export default function AboutPage() { +type Props = { + params: {locale: string}; +}; + +export default function AboutPage({params: {locale}}: Props) { + // Enable static rendering + unstable_setRequestLocale(locale); + const t = useTranslations('AboutPage'); 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 59ae19e19..6f30f9398 100644 --- a/examples/example-next-13/src/app/[locale]/layout.tsx +++ b/examples/example-next-13/src/app/[locale]/layout.tsx @@ -1,9 +1,10 @@ import clsx from 'clsx'; import {Inter} from 'next/font/google'; import {notFound} from 'next/navigation'; -import {createTranslator, NextIntlClientProvider} from 'next-intl'; +import {getTranslations, unstable_setRequestLocale} from 'next-intl/server'; import {ReactNode} from 'react'; import Navigation from 'components/Navigation'; +import {locales} from 'navigation'; const inter = Inter({subsets: ['latin']}); @@ -12,28 +13,17 @@ type Props = { params: {locale: string}; }; -async function getMessages(locale: string) { - try { - return (await import(`../../../messages/${locale}.json`)).default; - } catch (error) { - notFound(); - } +export function generateStaticParams() { + return locales.map((locale) => ({locale})); } -export async function generateStaticParams() { - return ['en', 'de'].map((locale) => ({locale})); -} - -export async function generateMetadata({params: {locale}}: Props) { - const messages = await getMessages(locale); - - // You can use the core (non-React) APIs when you have to use next-intl - // outside of components. Potentially this will be simplified in the future - // (see https://next-intl-docs.vercel.app/docs/next-13/server-components). - const t = createTranslator({locale, messages}); +export async function generateMetadata({ + params: {locale} +}: Omit) { + const t = await getTranslations({locale, namespace: 'LocaleLayout'}); return { - title: t('LocaleLayout.title') + title: t('title') }; } @@ -41,15 +31,17 @@ export default async function LocaleLayout({ children, params: {locale} }: Props) { - const messages = await getMessages(locale); + // Validate that the incoming `locale` parameter is valid + if (!locales.includes(locale as any)) notFound(); + + // Enable static rendering + unstable_setRequestLocale(locale); return ( - - - {children} - + + {children} ); diff --git a/examples/example-next-13/src/app/[locale]/not-found.tsx b/examples/example-next-13/src/app/[locale]/not-found.tsx index 089308a99..516ae1475 100644 --- a/examples/example-next-13/src/app/[locale]/not-found.tsx +++ b/examples/example-next-13/src/app/[locale]/not-found.tsx @@ -1,5 +1,3 @@ -'use client'; - import {useTranslations} from 'next-intl'; import PageLayout from 'components/PageLayout'; diff --git a/examples/example-next-13/src/app/[locale]/page.tsx b/examples/example-next-13/src/app/[locale]/page.tsx index bc3fec53e..f7ca6ce57 100644 --- a/examples/example-next-13/src/app/[locale]/page.tsx +++ b/examples/example-next-13/src/app/[locale]/page.tsx @@ -1,9 +1,21 @@ -'use client'; - +import {notFound} from 'next/navigation'; import {useTranslations} from 'next-intl'; +import {unstable_setRequestLocale} from 'next-intl/server'; import PageLayout from 'components/PageLayout'; +import {locales} from 'navigation'; + +type Props = { + params: {locale: string}; +}; + +export default function IndexPage({params: {locale}}: Props) { + // Validate that the incoming `locale` parameter is valid + const isValidLocale = locales.some((cur) => cur === locale); + if (!isValidLocale) notFound(); + + // Enable static rendering + unstable_setRequestLocale(locale); -export default function IndexPage() { const t = useTranslations('IndexPage'); return ( diff --git a/examples/example-next-13/src/components/LocaleSwitcher.tsx b/examples/example-next-13/src/components/LocaleSwitcher.tsx index 9e375ce5b..ac5f7821c 100644 --- a/examples/example-next-13/src/components/LocaleSwitcher.tsx +++ b/examples/example-next-13/src/components/LocaleSwitcher.tsx @@ -1,45 +1,17 @@ -'use client'; - -import clsx from 'clsx'; import {useLocale, useTranslations} from 'next-intl'; -import {usePathname, useRouter} from 'next-intl/client'; -import {ChangeEvent, useTransition} from 'react'; +import LocaleSwitcherSelect from './LocaleSwitcherSelect'; export default function LocaleSwitcher() { const t = useTranslations('LocaleSwitcher'); - const [isPending, startTransition] = useTransition(); const locale = useLocale(); - const router = useRouter(); - const pathname = usePathname(); - - function onSelectChange(event: ChangeEvent) { - const nextLocale = event.target.value; - startTransition(() => { - router.replace(pathname, {locale: nextLocale}); - }); - } return ( - + + {['en', 'de'].map((cur) => ( + + ))} + ); } diff --git a/examples/example-next-13/src/components/LocaleSwitcherSelect.tsx b/examples/example-next-13/src/components/LocaleSwitcherSelect.tsx new file mode 100644 index 000000000..4a5f2d292 --- /dev/null +++ b/examples/example-next-13/src/components/LocaleSwitcherSelect.tsx @@ -0,0 +1,48 @@ +'use client'; + +import clsx from 'clsx'; +import {ChangeEvent, ReactNode, useTransition} from 'react'; +import {useRouter, usePathname} from '../navigation'; + +type Props = { + children: ReactNode; + defaultValue: string; + label: string; +}; + +export default function LocaleSwitcherSelect({ + children, + defaultValue, + label +}: Props) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + const pathname = usePathname(); + + function onSelectChange(event: ChangeEvent) { + const nextLocale = event.target.value; + startTransition(() => { + router.replace(pathname, {locale: nextLocale}); + }); + } + + return ( + + ); +} diff --git a/examples/example-next-13/src/components/Navigation.spec.tsx b/examples/example-next-13/src/components/Navigation.spec.tsx index 0b5745d32..ba25fad02 100644 --- a/examples/example-next-13/src/components/Navigation.spec.tsx +++ b/examples/example-next-13/src/components/Navigation.spec.tsx @@ -15,9 +15,8 @@ jest.mock('next/navigation', () => ({ prefetch: jest.fn(), replace: jest.fn() }), - useParams: () => ({ - locale: 'en' - }) + useParams: () => ({locale: 'en'}), + useSelectedLayoutSegment: () => ({locale: 'en'}) })); it('renders', () => { diff --git a/examples/example-next-13/src/components/Navigation.tsx b/examples/example-next-13/src/components/Navigation.tsx index 004060003..0f286160b 100644 --- a/examples/example-next-13/src/components/Navigation.tsx +++ b/examples/example-next-13/src/components/Navigation.tsx @@ -1,5 +1,3 @@ -'use client'; - import {useTranslations} from 'next-intl'; import LocaleSwitcher from './LocaleSwitcher'; import NavigationLink from './NavigationLink'; diff --git a/examples/example-next-13/src/components/NavigationLink.tsx b/examples/example-next-13/src/components/NavigationLink.tsx index 705c345bc..0452db9ea 100644 --- a/examples/example-next-13/src/components/NavigationLink.tsx +++ b/examples/example-next-13/src/components/NavigationLink.tsx @@ -1,21 +1,20 @@ 'use client'; import clsx from 'clsx'; -import {usePathname} from 'next-intl/client'; -import Link from 'next-intl/link'; +import {useSelectedLayoutSegment} from 'next/navigation'; import {ComponentProps} from 'react'; +import {Link, pathnames} from '../navigation'; -type Props = Omit, 'href'> & { - href: string; -}; - -export default function NavigationLink({href, ...rest}: Props) { - const pathname = usePathname(); +export default function NavigationLink< + Pathname extends keyof typeof pathnames +>({href, ...rest}: ComponentProps>) { + const selectedLayoutSegment = useSelectedLayoutSegment(); + const pathname = selectedLayoutSegment ? `/${selectedLayoutSegment}` : '/'; const isActive = pathname === href; return ( ({ + messages: (await import(`../messages/${locale}.json`)).default +})); diff --git a/examples/example-next-13/src/middleware.ts b/examples/example-next-13/src/middleware.ts new file mode 100644 index 000000000..b3627145a --- /dev/null +++ b/examples/example-next-13/src/middleware.ts @@ -0,0 +1,13 @@ +import createMiddleware from 'next-intl/middleware'; +import {pathnames, locales} from './navigation'; + +export default createMiddleware({ + defaultLocale: 'en', + locales, + pathnames +}); + +export const config = { + // Match only internationalized pathnames + matcher: ['/', '/(de|en)/:path*'] +}; diff --git a/examples/example-next-13/src/navigation.ts b/examples/example-next-13/src/navigation.ts new file mode 100644 index 000000000..66c4a730e --- /dev/null +++ b/examples/example-next-13/src/navigation.ts @@ -0,0 +1,20 @@ +import { + createLocalizedPathnamesNavigation, + Pathnames +} from 'next-intl/navigation'; + +export const locales = ['en', 'de'] as const; + +export const pathnames = { + '/': '/', + '/about': { + en: '/about', + de: '/ueber' + } +} satisfies Pathnames; + +export const {Link, redirect, usePathname, useRouter} = + createLocalizedPathnamesNavigation({ + locales, + pathnames + }); diff --git a/examples/example-next-13/tests/main.spec.ts b/examples/example-next-13/tests/main.spec.ts index 69a887096..1d41d1d35 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'}); }); @@ -33,6 +33,14 @@ it("handles not found pages for routes that don't match the middleware", async ( page.getByRole('heading', {name: 'This page could not be found.'}); }); +it('sets caching headers', async ({request}) => { + 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' + ); + } +}); + it('can be used to configure metadata', async ({page}) => { await page.goto('/en'); await expect(page).toHaveTitle('next-intl example'); @@ -50,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/examples/example-remix/package.json b/examples/example-remix/package.json index 59090a3d2..7aae4942b 100644 --- a/examples/example-remix/package.json +++ b/examples/example-remix/package.json @@ -21,11 +21,11 @@ "devDependencies": { "@remix-run/dev": "^1.15.0", "@types/accept-language-parser": "^1.5.3", - "@types/react": "^18.2.5", + "@types/react": "^18.2.29", "@types/react-dom": "^18.2.1", "eslint": "^8.46.0", "eslint-config-molindo": "^7.0.0", - "typescript": "^5.0.0" + "typescript": "^5.2.2" }, "engines": { "node": ">=14" diff --git a/examples/example/package.json b/examples/example/package.json index ad5ed7508..3f4e8fec3 100644 --- a/examples/example/package.json +++ b/examples/example/package.json @@ -19,10 +19,10 @@ "devDependencies": { "@types/lodash": "^4.14.176", "@types/node": "^17.0.23", - "@types/react": "^18.2.5", + "@types/react": "^18.2.29", "eslint": "^8.46.0", "eslint-config-molindo": "^7.0.0", "eslint-config-next": "^13.4.0", - "typescript": "^5.0.0" + "typescript": "^5.2.2" } } diff --git a/examples/example/src/pages/_app.tsx b/examples/example/src/pages/_app.tsx index b5fec2769..8726c011b 100644 --- a/examples/example/src/pages/_app.tsx +++ b/examples/example/src/pages/_app.tsx @@ -1,9 +1,15 @@ import {AppProps} from 'next/app'; +import {useRouter} from 'next/router'; import {NextIntlClientProvider} from 'next-intl'; export default function App({Component, pageProps}: AppProps) { + const router = useRouter(); + return ( - + ); diff --git a/package.json b/package.json index 70bfce9f2..1c229a195 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,18 @@ }, "devDependencies": { "lerna": "^6.6.2", - "turbo": "^1.9.3" + "tsup": "^7.2.0", + "turbo": "^1.9.3", + "@babel/core": "^7.12.9", + "@babel/preset-env": "^7.22.14", + "@babel/preset-react": "^7.22.5", + "@babel/preset-typescript": "^7.22.11", + "@rollup/plugin-babel": "^6.0.3", + "@rollup/plugin-commonjs": "^25.0.4", + "@rollup/plugin-node-resolve": "^15.2.1", + "@rollup/plugin-replace": "^5.0.2", + "@rollup/plugin-terser": "^0.4.3", + "rollup": "^3.28.1", + "execa": "^5.1.1" } } diff --git a/packages/next-intl/.eslintrc.js b/packages/next-intl/.eslintrc.js index df42066d0..c96d722ca 100644 --- a/packages/next-intl/.eslintrc.js +++ b/packages/next-intl/.eslintrc.js @@ -1,5 +1,10 @@ module.exports = { - extends: ['molindo/typescript', 'molindo/react', 'molindo/jest'], + extends: [ + 'molindo/typescript', + 'molindo/react', + 'molindo/jest', + 'molindo/cypress' + ], plugins: ['deprecation'], overrides: [ { diff --git a/packages/next-intl/client.d.ts b/packages/next-intl/client.d.ts index 896ba758b..40656202e 100644 --- a/packages/next-intl/client.d.ts +++ b/packages/next-intl/client.d.ts @@ -1 +1 @@ -export * from './dist/client'; +export * from './dist/types/src/client'; diff --git a/packages/next-intl/config.d.ts b/packages/next-intl/config.d.ts new file mode 100644 index 000000000..86c346051 --- /dev/null +++ b/packages/next-intl/config.d.ts @@ -0,0 +1,3 @@ +import config from './dist/types/src/config'; + +export = config; diff --git a/packages/next-intl/dts.config.js b/packages/next-intl/dts.config.js index 82f1fcb7b..60121cc1d 100644 --- a/packages/next-intl/dts.config.js +++ b/packages/next-intl/dts.config.js @@ -1,14 +1,26 @@ -/* global module */ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-env node */ + +const preserveDirectives = require('rollup-plugin-preserve-directives').default; /** - * @type {import('dts-cli').DtsConfig} + * @type {import('dts-cli').DtsOptions} */ module.exports = { rollup(config) { - // Enable tree shaking detection in rollup / Bundlephobia - if (config.output.format === 'esm') { - config.output.preserveModules = true; - } + // 'use client' support + config.output.preserveModules = true; + config.plugins.push(preserveDirectives()); + config.onwarn = function onwarn(warning, warn) { + if (warning.code !== 'MODULE_LEVEL_DIRECTIVE') { + warn(warning); + } + }; + + // Otherwise rollup will insert code like `require('next/link')`, + // which will break the RSC render due to usage of `createContext` + config.treeshake.moduleSideEffects = false; + return config; } }; diff --git a/packages/next-intl/link.d.ts b/packages/next-intl/link.d.ts deleted file mode 100644 index 3c0dde466..000000000 --- a/packages/next-intl/link.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Link from './dist/link'; - -export = Link; diff --git a/packages/next-intl/middleware.d.ts b/packages/next-intl/middleware.d.ts index 0b95aa6d8..41dddf9a1 100644 --- a/packages/next-intl/middleware.d.ts +++ b/packages/next-intl/middleware.d.ts @@ -1,3 +1,3 @@ -import createMiddleware from './dist/middleware'; +import createMiddleware from './dist/types/src/middleware'; export = createMiddleware; diff --git a/packages/next-intl/navigation.d.ts b/packages/next-intl/navigation.d.ts new file mode 100644 index 000000000..bcbd563f4 --- /dev/null +++ b/packages/next-intl/navigation.d.ts @@ -0,0 +1 @@ +export * from './dist/types/src/navigation'; diff --git a/packages/next-intl/next-env.d.ts b/packages/next-intl/next-env.d.ts new file mode 100644 index 000000000..b963623f8 --- /dev/null +++ b/packages/next-intl/next-env.d.ts @@ -0,0 +1,6 @@ +// Normally Next.js creates this file when the build runs. Seems like from +// Next.js 13.2 on this is mandatory to be able to import `usePathname`. + +/// +/// +/// diff --git a/packages/next-intl/package.json b/packages/next-intl/package.json index e30748c82..f80405784 100644 --- a/packages/next-intl/package.json +++ b/packages/next-intl/package.json @@ -11,46 +11,51 @@ "url": "https://github.com/amannn/next-intl" }, "scripts": { - "build": "pnpm build:default && pnpm build:rsc", - "build:default": "rm -rf dist && dts build", - "build:rsc": "tsc && rm -rf dist/test", + "build": "rm -rf dist && rollup -c", "test": "TZ=Europe/Berlin vitest", "lint": "eslint src test && tsc --noEmit", "prepublishOnly": "CI=true turbo test && turbo lint && turbo build && cp ../../README.md .", "postpublish": "git checkout . && rm ./README.md", "size": "size-limit" }, - "main": "dist/index.js", - "module": "dist/next-intl.esm.js", - "typings": "dist/index.d.ts", + "main": "./dist/index.js", + "module": "./dist/esm/index.js", + "typings": "./dist/types/src/index.d.ts", "exports": { ".": { - "react-server": "./dist/src/react-server/index.js", + "types": "./dist/types/src/index.d.ts", + "react-server": "./dist/esm/index.react-server.js", "default": "./dist/index.js" }, "./server": { - "default": "./dist/src/server/index.js" + "types": "./server.d.ts", + "default": "./dist/esm/server.js" }, - "./client": { - "default": "./dist/src/client/index.js" - }, - "./link": { - "types": "./link.d.ts", - "react-server": "./dist/src/link/react-server/index.js", - "default": "./dist/src/link/index.js" + "./config": { + "types": "./config.d.ts", + "default": "./dist/config.js" }, "./middleware": { "types": "./middleware.d.ts", - "default": "./dist/src/middleware/index.js" + "default": "./dist/middleware.js" + }, + "./navigation": { + "types": "./navigation.d.ts", + "react-server": "./dist/esm/navigation.react-server.js", + "default": "./dist/navigation.js" + }, + "./plugin": { + "types": "./plugin.d.ts", + "default": "./dist/plugin.js" } }, "files": [ "dist", - "src", "server.d.ts", - "client.d.ts", - "link.d.ts", - "middleware.d.ts" + "navigation.d.ts", + "middleware.d.ts", + "plugin.d.ts", + "config.d.ts" ], "keywords": [ "react", @@ -80,8 +85,7 @@ "@testing-library/react": "^13.0.0", "@types/negotiator": "^0.6.1", "@types/node": "^17.0.23", - "@types/react": "^18.2.5", - "dts-cli": "^1.4.0", + "@types/react": "18.2.34", "eslint": "^8.46.0", "eslint-config-molindo": "^7.0.0", "eslint-plugin-deprecation": "^1.4.1", @@ -89,21 +93,28 @@ "path-to-regexp": "^6.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "rollup": "^3.28.1", + "rollup-plugin-preserve-directives": "0.2.0", "size-limit": "^8.2.6", - "typescript": "^5.0.0", + "typescript": "^5.2.2", "vitest": "^0.32.2" }, "size-limit": [ { - "path": "dist/next-intl.cjs.production.min.js", - "limit": "13.8 KB" + "path": "dist/production/index.js", + "limit": "12.81 KB" + }, + { + "path": "dist/production/navigation.js", + "limit": "2.6 KB" + }, + { + "path": "dist/production/server.js", + "limit": "13.2 kB" }, { - "path": "dist/next-intl.cjs.development.js", - "limit": "13.8 KB" + "path": "dist/production/middleware.js", + "limit": "5.7 KB" } - ], - "engines": { - "node": ">=10" - } + ] } diff --git a/packages/next-intl/plugin.d.ts b/packages/next-intl/plugin.d.ts new file mode 100644 index 000000000..59771d9b9 --- /dev/null +++ b/packages/next-intl/plugin.d.ts @@ -0,0 +1,6 @@ +import {NextConfig} from 'next'; + +function withNextIntl(i18nPath?: string): (config?: NextConfig) => NextConfig; + +// Currently only available via CJS +export = withNextIntl; diff --git a/packages/next-intl/rollup.config.js b/packages/next-intl/rollup.config.js new file mode 100644 index 000000000..2a773ca9e --- /dev/null +++ b/packages/next-intl/rollup.config.js @@ -0,0 +1,47 @@ +/* eslint-env node */ +const preserveDirectives = require('rollup-plugin-preserve-directives').default; +const getBuildConfig = require('../../scripts/getBuildConfig'); + +const config = { + input: { + index: 'src/index.tsx', + 'index.react-server': 'src/index.react-server.tsx', + + navigation: 'src/navigation.tsx', + 'navigation.react-server': 'src/navigation.react-server.tsx', + + server: 'src/server.tsx', + + middleware: 'src/middleware.tsx', + plugin: 'src/plugin.tsx', + config: 'src/config.tsx' + }, + external: ['next-intl/config', /use-intl/], + output: { + preserveModules: true + }, + onwarn(warning, warn) { + if (warning.code === 'MODULE_LEVEL_DIRECTIVE') return; + warn(warning); + }, + plugins: [preserveDirectives()] +}; + +module.exports = [ + getBuildConfig({ + ...config, + env: 'development' + }), + getBuildConfig({ + ...config, + output: { + ...config.output, + format: 'es' + }, + env: 'esm' + }), + getBuildConfig({ + ...config, + env: 'production' + }) +]; diff --git a/packages/next-intl/server.d.ts b/packages/next-intl/server.d.ts index 699fd532a..5d6991cd2 100644 --- a/packages/next-intl/server.d.ts +++ b/packages/next-intl/server.d.ts @@ -1 +1 @@ -export * from './dist/server'; +export * from './dist/types/src/server'; diff --git a/packages/next-intl/src/client/NextIntlClientProvider.tsx b/packages/next-intl/src/client/NextIntlClientProvider.tsx deleted file mode 100644 index 27d154c78..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 (!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 a186ee739..000000000 --- a/packages/next-intl/src/client/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Client-only APIs available via `next-intl/client`. - */ - -export {default as useRouter} from './useRouter'; -export {default as usePathname} from './usePathname'; - -// Legacy export (deprecation is handled by component) -export {default as NextIntlClientProvider} from './NextIntlClientProvider'; diff --git a/packages/next-intl/src/config.tsx b/packages/next-intl/src/config.tsx new file mode 100644 index 000000000..4839d56bf --- /dev/null +++ b/packages/next-intl/src/config.tsx @@ -0,0 +1,5 @@ +export default function getConfig() { + throw new Error( + "Couldn't find next-intl config file. Please follow the instructions at https://next-intl-docs.vercel.app/docs/getting-started/app-router-server-components" + ); +} diff --git a/packages/next-intl/src/index.react-server.tsx b/packages/next-intl/src/index.react-server.tsx new file mode 100644 index 000000000..812d94f58 --- /dev/null +++ b/packages/next-intl/src/index.react-server.tsx @@ -0,0 +1 @@ +export * from './react-server'; diff --git a/packages/next-intl/src/link/Link.tsx b/packages/next-intl/src/link/Link.tsx deleted file mode 100644 index a3cd66ac7..000000000 --- a/packages/next-intl/src/link/Link.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, {ComponentProps, forwardRef} from 'react'; -import useClientLocale from '../client/useClientLocale'; -import BaseLink from '../shared/BaseLink'; - -type Props = Omit, 'locale'> & { - locale?: string; -}; - -function Link({locale, ...rest}: Props, ref: Props['ref']) { - const defaultLocale = useClientLocale(); - return ; -} - -/** - * Wraps `next/link` and prefixes the `href` with the current locale if - * necessary. - * - * @example - * ```tsx - * import {Link} from 'next-intl'; - * - * // When the user is on `/en`, the link will point to `/en/about` - * About - * - * // You can override the `locale` to switch to another language - * Switch to German - * ``` - * - * Note that when a `locale` prop is passed to switch the locale, the `prefetch` - * prop is not supported. This is because Next.js would prefetch the page and - * 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. - */ -export default forwardRef(Link); 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/Link.tsx b/packages/next-intl/src/link/react-server/Link.tsx deleted file mode 100644 index b23efbaaa..000000000 --- a/packages/next-intl/src/link/react-server/Link.tsx +++ /dev/null @@ -1,6 +0,0 @@ -'use client'; - -// Allows to import 'next-intl/link' as a Client Component in -// Server Components as a stopgap solution until full RSC -// support is available for `next-intl/link`. -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.tsx b/packages/next-intl/src/middleware.tsx new file mode 100644 index 000000000..0b94d4d5a --- /dev/null +++ b/packages/next-intl/src/middleware.tsx @@ -0,0 +1 @@ +export {default} from './middleware/index'; diff --git a/packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx b/packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx index 7c29d95be..1b22f03d7 100644 --- a/packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx +++ b/packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx @@ -1,42 +1,54 @@ +import {AllLocales, Pathnames} from '../shared/types'; + type LocalePrefix = 'as-needed' | 'always' | 'never'; -type RoutingBaseConfig = { +type RoutingBaseConfig = { /** A list of all locales that are supported. */ - locales: Array; + locales: Locales; /* Used by default if none of the defined locales match. */ - defaultLocale: string; + defaultLocale: Locales[number]; /** 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`. */ localePrefix?: LocalePrefix; }; -export type DomainConfig = Omit< - RoutingBaseConfig, +export type DomainConfig = Omit< + RoutingBaseConfig, 'locales' | 'localePrefix' > & { /** The domain name (e.g. "example.com", "www.example.com" or "fr.example.com"). Note that the `x-forwarded-host` or alternatively the `host` header will be used to determine the requested domain. */ domain: string; - // Optional here - locales?: RoutingBaseConfig['locales']; -}; - -type MiddlewareConfig = RoutingBaseConfig & { - /** Can be used to change the locale handling per domain. */ - domains?: Array; - /** By setting this to `false`, the `accept-language` header will no longer be used for locale detection. */ - localeDetection?: boolean; - - /** 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; + /** The locales availabe on this particular domain. */ + locales?: RoutingBaseConfig>['locales']; }; -export type MiddlewareConfigWithDefaults = MiddlewareConfig & { - alternateLinks: boolean; - localePrefix: LocalePrefix; - localeDetection: boolean; -}; +type MiddlewareConfig = + RoutingBaseConfig & { + /** Can be used to change the locale handling per domain. */ + domains?: Array>; + + /** 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; + + /** By setting this to `false`, the `accept-language` header will no longer be used for locale detection. */ + localeDetection?: boolean; + + /** Maps internal pathnames to external ones which can be localized per locale. */ + pathnames?: Pathnames; + // Internal note: We want to accept this explicitly instead + // of inferring it from `next-intl/config` so that: + // a) The user gets TypeScript errors when there's a mismatch + // b) The middleware can be used in a standalone fashion + }; + +export type MiddlewareConfigWithDefaults = + MiddlewareConfig & { + alternateLinks: boolean; + localePrefix: LocalePrefix; + localeDetection: boolean; + }; export default MiddlewareConfig; diff --git a/packages/next-intl/src/middleware/getAlternateLinksHeaderValue.tsx b/packages/next-intl/src/middleware/getAlternateLinksHeaderValue.tsx index 108daa0ff..a6b8bfff7 100644 --- a/packages/next-intl/src/middleware/getAlternateLinksHeaderValue.tsx +++ b/packages/next-intl/src/middleware/getAlternateLinksHeaderValue.tsx @@ -1,34 +1,12 @@ import {NextRequest} from 'next/server'; -import MiddlewareConfig, { - MiddlewareConfigWithDefaults -} from './NextIntlMiddlewareConfig'; -import {getHost, isLocaleSupportedOnDomain} from './utils'; - -function getUnprefixedUrl(config: MiddlewareConfig, request: NextRequest) { - const url = new URL(request.url); - const host = getHost(request.headers); - if (host) { - url.port = ''; - url.host = host; - } - url.protocol = request.headers.get('x-forwarded-proto') ?? url.protocol; - - if (!url.pathname.endsWith('/')) { - url.pathname += '/'; - } - - url.pathname = url.pathname.replace( - new RegExp(`^/(${config.locales.join('|')})/`), - '/' - ); - - // Remove trailing slash - if (url.pathname !== '/') { - url.pathname = url.pathname.slice(0, -1); - } - - return url.toString(); -} +import {AllLocales, Pathnames} from '../shared/types'; +import {MiddlewareConfigWithDefaults} from './NextIntlMiddlewareConfig'; +import { + formatTemplatePathname, + getHost, + getNormalizedPathname, + isLocaleSupportedOnDomain +} from './utils'; function getAlternateEntry(url: string, locale: string) { return `<${url}>; rel="alternate"; hreflang="${locale}"`; @@ -37,23 +15,54 @@ function getAlternateEntry(url: string, locale: string) { /** * See https://developers.google.com/search/docs/specialty/international/localized-versions */ -export default function getAlternateLinksHeaderValue( - config: MiddlewareConfigWithDefaults, - request: NextRequest -) { - const unprefixedUrl = getUnprefixedUrl(config, request); +export default function getAlternateLinksHeaderValue< + Locales extends AllLocales +>({ + config, + localizedPathnames, + request, + resolvedLocale +}: { + config: MiddlewareConfigWithDefaults; + request: NextRequest; + resolvedLocale: Locales[number]; + localizedPathnames?: Pathnames[string]; +}) { + const normalizedUrl = request.nextUrl.clone(); + const host = getHost(request.headers); + if (host) { + normalizedUrl.port = ''; + normalizedUrl.host = host; + } + normalizedUrl.protocol = + request.headers.get('x-forwarded-proto') ?? normalizedUrl.protocol; + normalizedUrl.pathname = getNormalizedPathname( + normalizedUrl.pathname, + config.locales + ); + + function getLocalizedPathname(pathname: string, locale: Locales[number]) { + if (localizedPathnames && typeof localizedPathnames === 'object') { + return formatTemplatePathname( + pathname, + localizedPathnames[resolvedLocale], + localizedPathnames[locale] + ); + } else { + return pathname; + } + } const links = config.locales.flatMap((locale) => { - function localizePathname(url: URL) { - if (url.pathname === '/') { - url.pathname = `/${locale}`; + function prefixPathname(pathname: string) { + if (pathname === '/') { + return `/${locale}`; } else { - url.pathname = `/${locale}${url.pathname}`; + return `/${locale}${pathname}`; } - return url; } - let url; + let url: URL; if (config.domains) { const domainConfigs = @@ -62,24 +71,32 @@ export default function getAlternateLinksHeaderValue( ) || []; return domainConfigs.map((domainConfig) => { - url = new URL(unprefixedUrl); + url = new URL(normalizedUrl); url.port = ''; url.host = domainConfig.domain; + url.pathname = getLocalizedPathname(url.pathname, locale); if ( locale !== domainConfig.defaultLocale || config.localePrefix === 'always' ) { - localizePathname(url); + url.pathname = prefixPathname(url.pathname); } return getAlternateEntry(url.toString(), locale); }); } else { - url = new URL(unprefixedUrl); + let pathname: string; + if (localizedPathnames && typeof localizedPathnames === 'object') { + pathname = getLocalizedPathname(normalizedUrl.pathname, locale); + } else { + pathname = normalizedUrl.pathname; + } + if (locale !== config.defaultLocale || config.localePrefix === 'always') { - localizePathname(url); + pathname = prefixPathname(pathname); } + url = new URL(pathname, normalizedUrl); } return getAlternateEntry(url.toString(), locale); @@ -87,10 +104,13 @@ export default function getAlternateLinksHeaderValue( // Add x-default entry if (!config.domains) { - const url = new URL(unprefixedUrl); + const url = new URL( + getLocalizedPathname(normalizedUrl.pathname, config.defaultLocale), + normalizedUrl + ); links.push(getAlternateEntry(url.toString(), 'x-default')); } else { - // For `type: domain` there is no reasonable x-default + // For domain-based routing there is no reasonable x-default } return links.join(', '); diff --git a/packages/next-intl/src/middleware/middleware.tsx b/packages/next-intl/src/middleware/middleware.tsx index c4663156e..56ce519a3 100644 --- a/packages/next-intl/src/middleware/middleware.tsx +++ b/packages/next-intl/src/middleware/middleware.tsx @@ -1,42 +1,44 @@ import {NextRequest, NextResponse} from 'next/server'; -import {COOKIE_LOCALE_NAME} from '../shared/constants'; +import {COOKIE_LOCALE_NAME, HEADER_LOCALE_NAME} from '../shared/constants'; +import {AllLocales} from '../shared/types'; +import {matchesPathname} from '../shared/utils'; import MiddlewareConfig, { MiddlewareConfigWithDefaults } from './NextIntlMiddlewareConfig'; import getAlternateLinksHeaderValue from './getAlternateLinksHeaderValue'; import resolveLocale from './resolveLocale'; import { + getInternalTemplate, + formatTemplatePathname, + getBasePath, getBestMatchingDomain, - getLocaleFromPathname, + getKnownLocaleFromPathname, + getNormalizedPathname, + getPathWithSearch, isLocaleSupportedOnDomain } from './utils'; const ROOT_URL = '/'; -function receiveConfig(config: MiddlewareConfig) { - const result: MiddlewareConfigWithDefaults = { +function receiveConfig( + config: MiddlewareConfig +): MiddlewareConfigWithDefaults { + const result: MiddlewareConfigWithDefaults = { ...config, alternateLinks: config.alternateLinks ?? true, - localePrefix: config.localePrefix ?? 'as-needed', + localePrefix: config.localePrefix ?? 'always', localeDetection: config.localeDetection ?? true }; return result; } -export default function createMiddleware(config: MiddlewareConfig) { +export default function createMiddleware( + config: MiddlewareConfig +) { const configWithDefaults = receiveConfig(config); - // Currently only in use to enable a seamless upgrade path from the - // `{createIntlMiddleware} from 'next-intl/server'` API. - 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, @@ -44,7 +46,6 @@ export default function createMiddleware(config: MiddlewareConfig) { request.nextUrl.pathname ); - const isRoot = request.nextUrl.pathname === ROOT_URL; const hasOutdatedCookie = request.cookies.get(COOKIE_LOCALE_NAME)?.value !== locale; const hasMatchedDefaultLocale = domain @@ -58,23 +59,15 @@ export default function createMiddleware(config: MiddlewareConfig) { const hasUnknownHost = configWithDefaults.domains != null && !domain; function getResponseInit() { - const responseInit = { - request: { - headers: request.headers - } - }; - - return responseInit; + const headers = new Headers(request.headers); + headers.set(HEADER_LOCALE_NAME, locale); + return {request: {headers}}; } function rewrite(url: string) { return NextResponse.rewrite(new URL(url, request.url), getResponseInit()); } - function next() { - return NextResponse.next(getResponseInit()); - } - function redirect(url: string, host?: string) { const urlObj = new URL(url, request.url); @@ -106,79 +99,127 @@ export default function createMiddleware(config: MiddlewareConfig) { return NextResponse.redirect(urlObj.toString()); } + const normalizedPathname = getNormalizedPathname( + request.nextUrl.pathname, + configWithDefaults.locales + ); + + const pathLocale = getKnownLocaleFromPathname( + request.nextUrl.pathname, + configWithDefaults.locales + ); + const hasLocalePrefix = pathLocale != null; + let response; - if (isRoot) { - let pathWithSearch = `/${locale}`; - if (request.nextUrl.search) { - pathWithSearch += request.nextUrl.search; + let internalTemplateName: string | undefined; + + let pathname = request.nextUrl.pathname; + if (configWithDefaults.pathnames) { + let resolvedTemplateLocale; + [resolvedTemplateLocale = locale, internalTemplateName] = + getInternalTemplate(configWithDefaults.pathnames, normalizedPathname); + + if (internalTemplateName) { + const pathnameConfig = + configWithDefaults.pathnames[internalTemplateName]; + const localeTemplate: string = + typeof pathnameConfig === 'string' + ? pathnameConfig + : pathnameConfig[locale]; + + if (matchesPathname(localeTemplate, normalizedPathname)) { + pathname = formatTemplatePathname( + normalizedPathname, + localeTemplate, + internalTemplateName, + pathLocale + ); + } else { + const isDefaultLocale = + configWithDefaults.defaultLocale === locale || + domain?.defaultLocale === locale; + + response = redirect( + getPathWithSearch( + formatTemplatePathname( + normalizedPathname, + typeof pathnameConfig === 'string' + ? pathnameConfig + : pathnameConfig[resolvedTemplateLocale], + localeTemplate, + pathLocale || !isDefaultLocale ? locale : undefined + ), + request.nextUrl.search + ) + ); + } } + } - if ( - configWithDefaults.localePrefix === 'never' || - (hasMatchedDefaultLocale && - configWithDefaults.localePrefix === 'as-needed') - ) { - response = rewrite(pathWithSearch); + if (!response) { + if (pathname === ROOT_URL) { + const pathWithSearch = getPathWithSearch( + `/${locale}`, + request.nextUrl.search + ); + + if ( + configWithDefaults.localePrefix === 'never' || + (hasMatchedDefaultLocale && + configWithDefaults.localePrefix === 'as-needed') + ) { + response = rewrite(pathWithSearch); + } else { + response = redirect(pathWithSearch); + } } else { - response = redirect(pathWithSearch); - } - } else { - const pathLocaleCandidate = getLocaleFromPathname( - request.nextUrl.pathname - ); - const pathLocale = configWithDefaults.locales.includes( - pathLocaleCandidate - ) - ? pathLocaleCandidate - : undefined; - const hasLocalePrefix = pathLocale != null; - - let pathWithSearch = request.nextUrl.pathname; - if (request.nextUrl.search) { - pathWithSearch += request.nextUrl.search; - } + const pathWithSearch = getPathWithSearch( + pathname, + request.nextUrl.search + ); - if (hasLocalePrefix) { - const basePath = pathWithSearch.replace(`/${pathLocale}`, '') || '/'; + if (hasLocalePrefix) { + const basePath = getBasePath(pathWithSearch, pathLocale); - if (configWithDefaults.localePrefix === 'never') { - response = redirect(basePath); - } else if (pathLocale === locale) { - if ( - hasMatchedDefaultLocale && - configWithDefaults.localePrefix === 'as-needed' - ) { + if (configWithDefaults.localePrefix === 'never') { response = redirect(basePath); - } else { - if (configWithDefaults.domains) { - const pathDomain = getBestMatchingDomain( - domain, - pathLocale, - domainConfigs - ); - - if (domain?.domain !== pathDomain?.domain && !hasUnknownHost) { - response = redirect(basePath, pathDomain?.domain); + } else if (pathLocale === locale) { + if ( + hasMatchedDefaultLocale && + configWithDefaults.localePrefix === 'as-needed' + ) { + response = redirect(basePath); + } else { + if (configWithDefaults.domains) { + const pathDomain = getBestMatchingDomain( + domain, + pathLocale, + domainConfigs + ); + + if (domain?.domain !== pathDomain?.domain && !hasUnknownHost) { + response = redirect(basePath, pathDomain?.domain); + } else { + response = rewrite(pathWithSearch); + } } else { - response = next(); + response = rewrite(pathWithSearch); } - } else { - response = next(); } + } else { + response = redirect(`/${locale}${basePath}`); } } else { - response = redirect(`/${locale}${basePath}`); - } - } else { - if ( - configWithDefaults.localePrefix === 'never' || - (hasMatchedDefaultLocale && - (configWithDefaults.localePrefix === 'as-needed' || - configWithDefaults.domains)) - ) { - response = rewrite(`/${locale}${pathWithSearch}`); - } else { - response = redirect(`/${locale}${pathWithSearch}`); + if ( + configWithDefaults.localePrefix === 'never' || + (hasMatchedDefaultLocale && + (configWithDefaults.localePrefix === 'as-needed' || + configWithDefaults.domains)) + ) { + response = rewrite(`/${locale}${pathWithSearch}`); + } else { + response = redirect(`/${locale}${pathWithSearch}`); + } } } } @@ -197,7 +238,15 @@ export default function createMiddleware(config: MiddlewareConfig) { ) { response.headers.set( 'Link', - getAlternateLinksHeaderValue(configWithDefaults, request) + getAlternateLinksHeaderValue({ + config: configWithDefaults, + localizedPathnames: + internalTemplateName != null + ? configWithDefaults.pathnames?.[internalTemplateName] + : undefined, + request, + resolvedLocale: locale + }) ); } diff --git a/packages/next-intl/src/middleware/resolveLocale.tsx b/packages/next-intl/src/middleware/resolveLocale.tsx index 2698f2443..04a12edc4 100644 --- a/packages/next-intl/src/middleware/resolveLocale.tsx +++ b/packages/next-intl/src/middleware/resolveLocale.tsx @@ -2,6 +2,7 @@ import {match} from '@formatjs/intl-localematcher'; import Negotiator from 'negotiator'; import {RequestCookies} from 'next/dist/server/web/spec-extension/cookies'; import {COOKIE_LOCALE_NAME} from '../shared/constants'; +import {AllLocales} from '../shared/types'; import { DomainConfig, MiddlewareConfigWithDefaults @@ -12,9 +13,9 @@ import { isLocaleSupportedOnDomain } from './utils'; -function findDomainFromHost( +function findDomainFromHost( requestHeaders: Headers, - domains: Array + domains: Array> ) { let host = getHost(requestHeaders); @@ -28,9 +29,9 @@ function findDomainFromHost( return undefined; } -function getAcceptLanguageLocale( +function getAcceptLanguageLocale( requestHeaders: Headers, - locales: Array, + locales: Locales, defaultLocale: string ) { let locale; @@ -41,7 +42,11 @@ function getAcceptLanguageLocale( } }).languages(); try { - locale = match(languages, locales, defaultLocale); + locale = match( + languages, + locales as unknown as Array, + defaultLocale + ); } catch (e) { // Invalid language } @@ -49,8 +54,12 @@ function getAcceptLanguageLocale( return locale; } -function resolveLocaleFromPrefix( - {defaultLocale, localeDetection, locales}: MiddlewareConfigWithDefaults, +function resolveLocaleFromPrefix( + { + defaultLocale, + localeDetection, + locales + }: MiddlewareConfigWithDefaults, requestHeaders: Headers, requestCookies: RequestCookies, pathname: string @@ -88,8 +97,8 @@ function resolveLocaleFromPrefix( return locale; } -function resolveLocaleFromDomain( - config: MiddlewareConfigWithDefaults, +function resolveLocaleFromDomain( + config: MiddlewareConfigWithDefaults, requestHeaders: Headers, requestCookies: RequestCookies, pathname: string @@ -112,8 +121,10 @@ function resolveLocaleFromDomain( if (domain) { return { locale: - isLocaleSupportedOnDomain(localeFromPrefixStrategy, domain) || - hasLocalePrefix + isLocaleSupportedOnDomain( + localeFromPrefixStrategy, + domain + ) || hasLocalePrefix ? localeFromPrefixStrategy : domain.defaultLocale, domain @@ -125,12 +136,12 @@ function resolveLocaleFromDomain( return {locale: localeFromPrefixStrategy}; } -export default function resolveLocale( - config: MiddlewareConfigWithDefaults, +export default function resolveLocale( + config: MiddlewareConfigWithDefaults, requestHeaders: Headers, requestCookies: RequestCookies, pathname: string -): {locale: string; domain?: DomainConfig} { +): {locale: Locales[number]; domain?: DomainConfig} { if (config.domains) { return resolveLocaleFromDomain( config, diff --git a/packages/next-intl/src/middleware/utils.tsx b/packages/next-intl/src/middleware/utils.tsx index 33228ba35..08dd44649 100644 --- a/packages/next-intl/src/middleware/utils.tsx +++ b/packages/next-intl/src/middleware/utils.tsx @@ -1,9 +1,146 @@ -import {DomainConfig} from './NextIntlMiddlewareConfig'; +import {AllLocales} from '../shared/types'; +import {matchesPathname, templateToRegex} from '../shared/utils'; +import { + DomainConfig, + MiddlewareConfigWithDefaults +} from './NextIntlMiddlewareConfig'; export function getLocaleFromPathname(pathname: string) { return pathname.split('/')[1]; } +export function getInternalTemplate< + Locales extends AllLocales, + Pathnames extends NonNullable< + MiddlewareConfigWithDefaults['pathnames'] + > +>( + pathnames: Pathnames, + pathname: string +): [Locales[number] | undefined, keyof Pathnames | undefined] { + for (const [internalPathname, localizedPathnamesOrPathname] of Object.entries( + pathnames + )) { + if (typeof localizedPathnamesOrPathname === 'string') { + const localizedPathname = localizedPathnamesOrPathname; + if (matchesPathname(localizedPathname, pathname)) { + return [undefined, internalPathname]; + } + } else { + for (const [locale, localizedPathname] of Object.entries( + localizedPathnamesOrPathname + )) { + if (matchesPathname(localizedPathname as string, pathname)) { + return [locale, internalPathname]; + } + } + } + } + + return [undefined, undefined]; +} + +export function formatTemplatePathname( + sourcePathname: string, + sourceTemplate: string, + targetTemplate: string, + localePrefix?: string +) { + const params = getRouteParams(sourceTemplate, sourcePathname); + let targetPathname = ''; + if (localePrefix) { + targetPathname = `/${localePrefix}`; + } + targetPathname += formatPathname(targetTemplate, params); + + if (targetPathname.endsWith('/')) { + targetPathname = targetPathname.slice(0, -1); + } + + return targetPathname; +} + +/** + * Removes potential locales from the pathname. + */ +export function getNormalizedPathname( + pathname: string, + locales: Locales +) { + // Add trailing slash for consistent handling + // both for the root as well as nested paths + if (!pathname.endsWith('/')) { + pathname += '/'; + } + + const match = pathname.match(`^/(${locales.join('|')})(.*)`); + let result = match ? match[2] : pathname; + + // Remove trailing slash + if (result.endsWith('/') && result !== '/') { + result = result.slice(0, -1); + } + + return result; +} + +export function getKnownLocaleFromPathname( + pathname: string, + locales: Locales +): Locales[number] | undefined { + const pathLocaleCandidate = getLocaleFromPathname(pathname); + const pathLocale = locales.includes(pathLocaleCandidate) + ? pathLocaleCandidate + : undefined; + return pathLocale; +} + +export function getBasePath(pathname: string, pathLocale: string) { + let result = pathname.replace(`/${pathLocale}`, ''); + if (!result.startsWith('/')) { + result = `/${result}`; + } + return result; +} + +export function getRouteParams(template: string, pathname: string) { + const regex = templateToRegex(template); + const match = regex.exec(pathname); + if (!match) return undefined; + const params: Record = {}; + for (let i = 1; i < match.length; i++) { + const key = template.match(/\[([^\]]+)\]/g)?.[i - 1].replace(/[[\]]/g, ''); + if (key) params[key] = match[i]; + } + return params; +} + +export function formatPathname(template: string, params?: object) { + if (!params) return template; + + // Simplify syntax for optional catchall ('[[...slug]]') so + // we can replace the value with simple interpolation + template = template.replaceAll('[[', '[').replaceAll(']]', ']'); + + let result = template; + Object.entries(params).forEach(([key, value]) => { + result = result.replace(`[${key}]`, value); + }); + + return result; +} + +export function getPathWithSearch( + pathname: string, + search: string | undefined +) { + let pathWithSearch = pathname; + if (search) { + pathWithSearch += search; + } + return pathWithSearch; +} + export function getHost(requestHeaders: Headers) { return ( requestHeaders.get('x-forwarded-host') ?? @@ -12,9 +149,9 @@ export function getHost(requestHeaders: Headers) { ); } -export function isLocaleSupportedOnDomain( +export function isLocaleSupportedOnDomain( locale: string, - domain: DomainConfig + domain: DomainConfig ) { return ( domain.defaultLocale === locale || @@ -23,10 +160,10 @@ export function isLocaleSupportedOnDomain( ); } -export function getBestMatchingDomain( - curHostDomain: DomainConfig | undefined, +export function getBestMatchingDomain( + curHostDomain: DomainConfig | undefined, locale: string, - domainConfigs: Array + domainConfigs: Array> ) { let domainConfig; diff --git a/packages/next-intl/src/navigation.react-server.tsx b/packages/next-intl/src/navigation.react-server.tsx new file mode 100644 index 000000000..c207e942e --- /dev/null +++ b/packages/next-intl/src/navigation.react-server.tsx @@ -0,0 +1 @@ +export * from './navigation/react-server'; diff --git a/packages/next-intl/src/navigation.tsx b/packages/next-intl/src/navigation.tsx new file mode 100644 index 000000000..22efa4cbc --- /dev/null +++ b/packages/next-intl/src/navigation.tsx @@ -0,0 +1 @@ +export * from './navigation/index'; diff --git a/packages/next-intl/src/navigation/BaseLink.tsx b/packages/next-intl/src/navigation/BaseLink.tsx new file mode 100644 index 000000000..6de67da93 --- /dev/null +++ b/packages/next-intl/src/navigation/BaseLink.tsx @@ -0,0 +1,53 @@ +import React, {ComponentProps, ReactElement, forwardRef} from 'react'; +import useLocale from '../react-client/useLocale'; +import BaseLinkWithLocale from '../shared/BaseLinkWithLocale'; +import {AllLocales} from '../shared/types'; + +type Props = Omit< + ComponentProps, + 'locale' +> & { + locale?: Locales[number]; +}; + +function BaseLink( + {locale, ...rest}: Props, + ref: Props['ref'] +) { + const defaultLocale = useLocale(); + const linkLocale = locale || defaultLocale; + return ( + + ); +} + +/** + * Wraps `next/link` and prefixes the `href` with the current locale if + * necessary. + * + * @example + * ```tsx + * import {Link} from 'next-intl'; + * + * // When the user is on `/en`, the link will point to `/en/about` + * About + * + * // You can override the `locale` to switch to another language + * Switch to German + * ``` + * + * Note that when a `locale` prop is passed to switch the locale, the `prefetch` + * prop is not supported. This is because Next.js would prefetch the page and + * 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 BaseLinkWithRef = forwardRef(BaseLink) as ( + props: Props & {ref?: Props['ref']} +) => ReactElement; +(BaseLinkWithRef as any).displayName = 'Link'; +export default BaseLinkWithRef; diff --git a/packages/next-intl/src/navigation/StrictParams.tsx b/packages/next-intl/src/navigation/StrictParams.tsx new file mode 100644 index 000000000..ed4332b02 --- /dev/null +++ b/packages/next-intl/src/navigation/StrictParams.tsx @@ -0,0 +1,27 @@ +type ParamValue = string | number | boolean; + +type ReadFrom = Path extends `${string}[${infer Rest}` + ? ReadUntil + : []; + +type ReadUntil = Path extends `${infer Match}]${infer Rest}` + ? [Match, ...ReadFrom] + : []; + +type RemovePrefixes = Key extends `[...${infer Name}` + ? Name + : Key extends `...${infer Name}` + ? Name + : Key; + +type StrictParams = Pathname extends `${string}[${string}` + ? { + [Key in ReadFrom[number] as RemovePrefixes]: Key extends `[...${string}` + ? Array | undefined + : Key extends `...${string}` + ? Array + : ParamValue; + } + : never; + +export default StrictParams; diff --git a/packages/next-intl/src/navigation/baseRedirect.tsx b/packages/next-intl/src/navigation/baseRedirect.tsx new file mode 100644 index 000000000..05c739244 --- /dev/null +++ b/packages/next-intl/src/navigation/baseRedirect.tsx @@ -0,0 +1,22 @@ +import useLocale from '../react-client/useLocale'; +import redirectWithLocale from '../shared/redirectWithLocale'; +import {ParametersExceptFirstTwo} from '../shared/types'; + +export default function baseRedirect( + pathname: string, + ...args: ParametersExceptFirstTwo +) { + let locale; + try { + // eslint-disable-next-line react-hooks/rules-of-hooks -- Reading from context here is fine, since `redirect` should be called during render + locale = useLocale(); + } catch (e) { + throw new Error( + process.env.NODE_ENV !== 'production' + ? '`redirect()` can only be called during render. To redirect in an event handler or similar, you can use `useRouter()` instead.' + : undefined + ); + } + + return redirectWithLocale(pathname, locale, ...args); +} diff --git a/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx b/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx new file mode 100644 index 000000000..5d35ec9a5 --- /dev/null +++ b/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx @@ -0,0 +1,148 @@ +import React, {ComponentProps, ReactElement, forwardRef} from 'react'; +import useLocale from '../react-client/useLocale'; +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, + normalizeNameOrNameWithParams, + HrefOrHrefWithParams, + HrefOrUrlObjectWithParams +} from './utils'; + +export default function createLocalizedPathnamesNavigation< + Locales extends AllLocales, + PathnamesConfig extends Pathnames +>({locales, pathnames}: {locales: Locales; pathnames: PathnamesConfig}) { + 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< + ComponentProps, + 'href' | 'name' + > & { + href: HrefOrUrlObjectWithParams; + locale?: Locales[number]; + }; + function Link( + {href, locale, ...rest}: LinkProps, + ref?: ComponentProps['ref'] + ) { + const defaultLocale = useTypedLocale(); + const finalLocale = locale || defaultLocale; + + return ( + ({ + locale: finalLocale, + // @ts-expect-error -- This is ok + pathname: href, + // @ts-expect-error -- This is ok + params: typeof href === 'object' ? href.params : undefined, + pathnames + })} + locale={locale} + {...rest} + /> + ); + } + const LinkWithRef = forwardRef(Link) as unknown as < + Pathname extends keyof PathnamesConfig + >( + props: LinkProps & {ref?: ComponentProps['ref']} + ) => ReactElement; + (LinkWithRef as any).displayName = 'Link'; + + function redirect( + href: HrefOrHrefWithParams, + ...args: ParametersExceptFirst + ) { + // eslint-disable-next-line react-hooks/rules-of-hooks -- Reading from context here is fine, since `redirect` should be called during render + const locale = useTypedLocale(); + const resolvedHref = getPathname({href, locale}); + return baseRedirect(resolvedHref, ...args); + } + + function useRouter() { + const baseRouter = useBaseRouter(); + const defaultLocale = useTypedLocale(); + + return { + ...baseRouter, + push( + href: HrefOrHrefWithParams, + ...args: ParametersExceptFirst + ) { + const resolvedHref = getPathname({ + href, + locale: args[0]?.locale || defaultLocale + }); + return baseRouter.push(resolvedHref, ...args); + }, + + replace( + href: HrefOrHrefWithParams, + ...args: ParametersExceptFirst + ) { + const resolvedHref = getPathname({ + href, + locale: args[0]?.locale || defaultLocale + }); + return baseRouter.replace(resolvedHref, ...args); + }, + + prefetch( + href: HrefOrHrefWithParams, + ...args: ParametersExceptFirst + ) { + const resolvedHref = getPathname({ + href, + locale: args[0]?.locale || defaultLocale + }); + return baseRouter.prefetch(resolvedHref, ...args); + } + }; + } + + function usePathname(): keyof PathnamesConfig { + const pathname = useBasePathname(); + const locale = useTypedLocale(); + return getRoute({pathname, locale, pathnames}); + } + + function getPathname({ + href, + locale + }: { + locale: Locales[number]; + href: HrefOrHrefWithParams; + }) { + return compileLocalizedPathname({ + ...normalizeNameOrNameWithParams(href), + locale, + pathnames + }); + } + + return { + Link: LinkWithRef, + redirect, + usePathname, + useRouter, + getPathname + }; +} diff --git a/packages/next-intl/src/navigation/createSharedPathnamesNavigation.tsx b/packages/next-intl/src/navigation/createSharedPathnamesNavigation.tsx new file mode 100644 index 000000000..4ca8a2150 --- /dev/null +++ b/packages/next-intl/src/navigation/createSharedPathnamesNavigation.tsx @@ -0,0 +1,17 @@ +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: 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 new file mode 100644 index 000000000..302f92421 --- /dev/null +++ b/packages/next-intl/src/navigation/index.tsx @@ -0,0 +1,3 @@ +export {default as createSharedPathnamesNavigation} from './createSharedPathnamesNavigation'; +export {default as createLocalizedPathnamesNavigation} from './createLocalizedPathnamesNavigation'; +export type {Pathnames} from '../shared/types'; diff --git a/packages/next-intl/src/navigation/react-server/BaseLink.tsx b/packages/next-intl/src/navigation/react-server/BaseLink.tsx new file mode 100644 index 000000000..bcec3a8ea --- /dev/null +++ b/packages/next-intl/src/navigation/react-server/BaseLink.tsx @@ -0,0 +1,19 @@ +import React, {ComponentProps} from 'react'; +import useLocale from '../../react-server/useLocale'; +import BaseLinkWithLocale from '../../shared/BaseLinkWithLocale'; +import {AllLocales} from '../../shared/types'; + +type Props = Omit< + ComponentProps, + 'locale' +> & { + locale?: Locales[number]; +}; + +export default function BaseLink({ + locale, + ...rest +}: Props) { + const defaultLocale = useLocale(); + 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 new file mode 100644 index 000000000..4725e5fef --- /dev/null +++ b/packages/next-intl/src/navigation/react-server/createLocalizedPathnamesNavigation.tsx @@ -0,0 +1,86 @@ +import React, {ComponentProps} from 'react'; +import {getRequestLocale} from '../../server/RequestLocale'; +import {AllLocales, ParametersExceptFirst, Pathnames} from '../../shared/types'; +import { + HrefOrHrefWithParams, + HrefOrUrlObjectWithParams, + compileLocalizedPathname, + normalizeNameOrNameWithParams +} from '../utils'; +import BaseLink from './BaseLink'; +import baseRedirect from './baseRedirect'; + +export default function createLocalizedPathnamesNavigation< + Locales extends AllLocales, + PathnamesConfig extends Pathnames +>({locales, pathnames}: {locales: Locales; pathnames: Pathnames}) { + type LinkProps = Omit< + ComponentProps, + 'href' | 'name' + > & { + href: HrefOrUrlObjectWithParams; + locale?: Locales[number]; + }; + function Link({ + href, + locale, + ...rest + }: LinkProps) { + const defaultLocale = getRequestLocale() as (typeof locales)[number]; + const finalLocale = locale || defaultLocale; + + return ( + ({ + locale: finalLocale, + // @ts-expect-error -- This is ok + pathname: href, + // @ts-expect-error -- This is ok + params: typeof href === 'object' ? href.params : undefined, + pathnames + })} + locale={locale} + {...rest} + /> + ); + } + + function redirect( + href: HrefOrHrefWithParams, + ...args: ParametersExceptFirst + ) { + const locale = getRequestLocale(); + const resolvedHref = getPathname({href, locale}); + return baseRedirect(resolvedHref, ...args); + } + + function getPathname({ + href, + locale + }: { + locale: Locales[number]; + href: HrefOrHrefWithParams; + }) { + return compileLocalizedPathname({ + ...normalizeNameOrNameWithParams(href), + locale, + pathnames + }); + } + + function notSupported(message: string) { + return () => { + throw new Error( + `\`${message}\` is not supported in Server Components. You can use this hook if you convert the component to a Client Component.` + ); + }; + } + + return { + Link, + redirect, + getPathname, + usePathname: notSupported('usePathname'), + useRouter: notSupported('useRouter') + }; +} diff --git a/packages/next-intl/src/navigation/react-server/createSharedPathnamesNavigation.tsx b/packages/next-intl/src/navigation/react-server/createSharedPathnamesNavigation.tsx new file mode 100644 index 000000000..58458c2cd --- /dev/null +++ b/packages/next-intl/src/navigation/react-server/createSharedPathnamesNavigation.tsx @@ -0,0 +1,23 @@ +import {AllLocales} from '../../shared/types'; +import BaseLink from './BaseLink'; +import baseRedirect from './baseRedirect'; + +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}) { + function notSupported(message: string) { + return () => { + throw new Error( + `\`${message}\` is not supported in Server Components. You can use this hook if you convert the component to a Client Component.` + ); + }; + } + + return { + 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 new file mode 100644 index 000000000..ebfc28f20 --- /dev/null +++ b/packages/next-intl/src/navigation/react-server/index.tsx @@ -0,0 +1,3 @@ +export {default as createSharedPathnamesNavigation} from './createSharedPathnamesNavigation'; +export {default as createLocalizedPathnamesNavigation} from './createLocalizedPathnamesNavigation'; +export type {Pathnames} from '../../shared/types'; diff --git a/packages/next-intl/src/client/usePathname.tsx b/packages/next-intl/src/navigation/useBasePathname.tsx similarity index 88% rename from packages/next-intl/src/client/usePathname.tsx rename to packages/next-intl/src/navigation/useBasePathname.tsx index 1420e46ff..e4625a09b 100644 --- a/packages/next-intl/src/client/usePathname.tsx +++ b/packages/next-intl/src/navigation/useBasePathname.tsx @@ -2,8 +2,8 @@ import {usePathname as useNextPathname} from 'next/navigation'; import {useMemo} from 'react'; +import useLocale from '../react-client/useLocale'; import {hasPathnamePrefixed, unlocalizePathname} from '../shared/utils'; -import useClientLocale from './useClientLocale'; /** * Returns the pathname without a potential locale prefix. @@ -18,14 +18,14 @@ import useClientLocale from './useClientLocale'; * 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< typeof useNextPathname > | null; - const locale = useClientLocale(); + const locale = useLocale(); return useMemo(() => { if (!pathname) return pathname as ReturnType; diff --git a/packages/next-intl/src/client/useRouter.tsx b/packages/next-intl/src/navigation/useBaseRouter.tsx similarity index 79% rename from packages/next-intl/src/client/useRouter.tsx rename to packages/next-intl/src/navigation/useBaseRouter.tsx index 9d2c43dc3..1c5569e62 100644 --- a/packages/next-intl/src/client/useRouter.tsx +++ b/packages/next-intl/src/navigation/useBaseRouter.tsx @@ -1,10 +1,11 @@ import {useRouter as useNextRouter} from 'next/navigation'; import {useMemo} from 'react'; +import useLocale from '../react-client/useLocale'; +import {AllLocales} from '../shared/types'; import {localizeHref} from '../shared/utils'; -import useClientLocale from './useClientLocale'; -type IntlNavigateOptions = { - locale?: string; +type IntlNavigateOptions = { + locale?: Locales[number]; }; /** @@ -26,9 +27,9 @@ type IntlNavigateOptions = { * router.push('/about', {locale: 'de'}); * ``` */ -export default function useRouter() { +export default function useBaseRouter() { const router = useNextRouter(); - const locale = useClientLocale(); + const locale = useLocale(); return useMemo(() => { function localize(href: string, nextLocale?: string) { @@ -44,7 +45,8 @@ export default function useRouter() { ...router, push( href: string, - options?: Parameters[1] & IntlNavigateOptions + options?: Parameters[1] & + IntlNavigateOptions ) { const {locale: nextLocale, ...rest} = options || {}; const args: [ @@ -59,7 +61,8 @@ export default function useRouter() { replace( href: string, - options?: Parameters[1] & IntlNavigateOptions + options?: Parameters[1] & + IntlNavigateOptions ) { const {locale: nextLocale, ...rest} = options || {}; const args: [ @@ -74,7 +77,8 @@ export default function useRouter() { prefetch( href: string, - options?: Parameters[1] & IntlNavigateOptions + options?: Parameters[1] & + IntlNavigateOptions ) { const {locale: nextLocale, ...rest} = options || {}; const args: [ diff --git a/packages/next-intl/src/navigation/utils.tsx b/packages/next-intl/src/navigation/utils.tsx new file mode 100644 index 000000000..713640ebc --- /dev/null +++ b/packages/next-intl/src/navigation/utils.tsx @@ -0,0 +1,177 @@ +import type {ParsedUrlQueryInput} from 'node:querystring'; +import type {UrlObject} from 'url'; +import {AllLocales, Pathnames} from '../shared/types'; +import {matchesPathname, unlocalizePathname} from '../shared/utils'; +import StrictParams from './StrictParams'; + +type SearchParamValue = ParsedUrlQueryInput[keyof ParsedUrlQueryInput]; + +// Minor false positive: A route that has both optional and +// required params will allow optional params. +type HrefOrHrefWithParamsImpl = + Pathname extends `${string}[[...${string}` + ? // Optional catch-all + Pathname | ({pathname: Pathname; params?: StrictParams} & Other) + : Pathname extends `${string}[${string}` + ? // Required catch-all & regular params + {pathname: Pathname; params: StrictParams} & Other + : // No params + Pathname | ({pathname: Pathname} & Other); + +export type HrefOrUrlObjectWithParams = HrefOrHrefWithParamsImpl< + Pathname, + Omit +>; + +export type HrefOrHrefWithParams = HrefOrHrefWithParamsImpl< + Pathname, + {query?: Record} +>; + +export function normalizeNameOrNameWithParams( + href: HrefOrHrefWithParams +): { + pathname: Pathname; + params?: StrictParams; +} { + // @ts-expect-error -- `extends string` in the generic unfortunately weakens the type + return typeof href === 'string' ? {pathname: href as Pathname} : href; +} + +export function serializeSearchParams( + searchParams: Record +) { + function serializeValue(value: SearchParamValue) { + return String(value); + } + + const urlSearchParams = new URLSearchParams(); + for (const [key, value] of Object.entries(searchParams)) { + if (Array.isArray(value)) { + value.forEach((cur) => { + urlSearchParams.append(key, serializeValue(cur)); + }); + } else { + urlSearchParams.set(key, serializeValue(value)); + } + } + return '?' + urlSearchParams.toString(); +} + +type StrictUrlObject = Omit & { + pathname: Pathname; +}; + +export function compileLocalizedPathname< + Locales extends AllLocales, + Pathname +>(opts: { + locale: Locales[number]; + pathname: Pathname; + params?: StrictParams; + pathnames: Pathnames; + query?: Record; +}): string; +export function compileLocalizedPathname< + Locales extends AllLocales, + Pathname +>(opts: { + locale: Locales[number]; + pathname: StrictUrlObject; + params?: StrictParams; + pathnames: Pathnames; + query?: Record; +}): UrlObject; +export function compileLocalizedPathname({ + pathname, + locale, + params, + pathnames, + query +}: { + locale: Locales[number]; + pathname: keyof typeof pathnames | StrictUrlObject; + params?: StrictParams; + pathnames: Pathnames; + query?: Record; +}) { + function getNamedPath(value: keyof typeof pathnames) { + let namedPath = pathnames[value]; + if (!namedPath) { + namedPath = value; + } + return namedPath; + } + + function compilePath( + namedPath: Pathnames[keyof Pathnames] + ) { + const template = + typeof namedPath === 'string' ? namedPath : namedPath[locale]; + let compiled = template; + + if (params) { + Object.entries(params).forEach(([key, value]) => { + if (Array.isArray(value)) { + compiled = compiled.replace( + new RegExp(`(\\[)?\\[...${key}\\](\\])?`, 'g'), + value.map((v) => String(v)).join('/') + ); + } else { + compiled = compiled.replace(`[${key}]`, String(value)); + } + }); + } + + if (process.env.NODE_ENV !== 'production' && compiled.includes('[')) { + // Next.js throws anyway, therefore better provide a more helpful error message + throw new Error( + `Insufficient params provided for localized pathname.\nTemplate: ${template}\nParams: ${JSON.stringify( + params + )}` + ); + } + + if (query) { + compiled += serializeSearchParams(query); + } + + return compiled; + } + + if (typeof pathname === 'string') { + const namedPath = getNamedPath(pathname); + const compiled = compilePath(namedPath); + return compiled; + } else { + const {pathname: href, ...rest} = pathname; + const namedPath = getNamedPath(href); + const compiled = compilePath(namedPath); + const result: UrlObject = {...rest, pathname: compiled}; + return result; + } +} + +export function getRoute({ + locale, + pathname, + pathnames +}: { + locale: Locales[number]; + pathname: string; + pathnames: Pathnames; +}) { + const unlocalizedPathname = unlocalizePathname(pathname, locale); + + let template = Object.entries(pathnames).find(([, routePath]) => { + const routePathname = + typeof routePath !== 'string' ? routePath[locale] : routePath; + return matchesPathname(routePathname, unlocalizedPathname); + })?.[0]; + + if (!template) { + template = pathname; + } + + return template as keyof Pathnames; +} diff --git a/packages/next-intl/src/plugin.tsx b/packages/next-intl/src/plugin.tsx new file mode 100644 index 000000000..b3f16e795 --- /dev/null +++ b/packages/next-intl/src/plugin.tsx @@ -0,0 +1,74 @@ +/* eslint-env node */ + +import fs from 'fs'; +import path from 'path'; +import type {NextConfig} from 'next'; + +function resolveI18nPath(cwd: string, providedPath?: string) { + let i18nPath = providedPath; + + if (i18nPath) { + i18nPath = path.resolve(i18nPath); + + if (!fs.existsSync(i18nPath)) { + throw new Error( + `Could not find i18n config at ${i18nPath}, please provide a valid path.` + ); + } + } else { + i18nPath = [ + './i18n.tsx', + './i18n.ts', + './i18n.js', + './i18n.jsx', + './src/i18n.tsx', + './src/i18n.ts', + './src/i18n.js', + './src/i18n.jsx' + ] + .map((cur) => path.resolve(cwd, cur)) + .find((cur) => fs.existsSync(cur)); + + if (!i18nPath) { + throw new Error(`\n\nCould not locate i18n config. Create one at \`./(src/)i18n.{js,jsx,ts,tsx}\` or specify a custom location: + +const withNextIntl = require('next-intl/plugin')( + './path/to/i18n.tsx' +); + +module.exports = withNextIntl({ + // Other Next.js configuration ... +});\n`); + } + } + + return i18nPath; +} + +function initPlugin(i18nPath?: string, nextConfig?: NextConfig): NextConfig { + if (nextConfig?.i18n != null) { + console.warn( + "\nnext-intl has found an `i18n` config in your next.config.js. This likely causes conflicts and should therefore be removed if you use the App Router.\n\nIf you're in progress of migrating from the `pages` folder, you can refer to this example: https://github.com/amannn/next-intl/tree/feat/next-13-rsc/packages/example-next-13-with-pages\n" + ); + } + + return Object.assign({}, nextConfig, { + webpack( + ...[config, options]: Parameters> + ) { + config.resolve.alias['next-intl/config'] = require.resolve( + resolveI18nPath(config.context, i18nPath) + ); + + if (typeof nextConfig?.webpack === 'function') { + return nextConfig.webpack(config, options); + } + + return config; + } + }); +} + +module.exports = function withNextIntl(i18nPath?: string) { + return (nextConfig?: NextConfig) => initPlugin(i18nPath, nextConfig); +}; diff --git a/packages/next-intl/src/react-client/index.tsx b/packages/next-intl/src/react-client/index.tsx index 5ee8909e5..a2f4f5ee8 100644 --- a/packages/next-intl/src/react-client/index.tsx +++ b/packages/next-intl/src/react-client/index.tsx @@ -8,8 +8,45 @@ * supported in all Next.js versions that are supported. */ +import { + useTranslations as base_useTranslations, + useFormatter as base_useFormatter +} from 'use-intl'; + export * from 'use-intl'; -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'; +// eslint-disable-next-line @typescript-eslint/ban-types +function callHook(name: string, hook: Function) { + return (...args: Array) => { + try { + return hook(...args); + } catch (e) { + throw new Error( + process.env.NODE_ENV !== 'production' + ? `Failed to call \`${name}\` because the context from \`NextIntlClientProvider\` was not found. + +This can happen because: +1) You intended to render this component as a Server Component, the render + failed, and therefore React attempted to render the component on the client + instead. If this is the case, check the console for server errors. +2) You intended to render this component on the client side, but no context was found. + Learn more about this error here: https://next-intl-docs.vercel.app/docs/environments/server-client-components#missing-context` + : undefined + ); + } + }; +} + +export const useTranslations = callHook( + 'useTranslations', + base_useTranslations +) as typeof base_useTranslations; +export const useFormatter = callHook( + 'useFormatter', + base_useFormatter +) as typeof base_useFormatter; + +// Replace `useLocale` export from `use-intl` +export {default as useLocale} from './useLocale'; + +export {default as NextIntlClientProvider} from '../shared/NextIntlClientProvider'; diff --git a/packages/next-intl/src/client/useClientLocale.tsx b/packages/next-intl/src/react-client/useLocale.tsx similarity index 65% rename from packages/next-intl/src/client/useClientLocale.tsx rename to packages/next-intl/src/react-client/useLocale.tsx index a407fe031..ba95f213b 100644 --- a/packages/next-intl/src/client/useClientLocale.tsx +++ b/packages/next-intl/src/react-client/useLocale.tsx @@ -1,8 +1,9 @@ import {useParams} from 'next/navigation'; -import {useLocale} from 'use-intl'; +// Workaround for some bundle splitting until we have ESM +import {useLocale as useBaseLocale} from 'use-intl/_useLocale'; import {LOCALE_SEGMENT_NAME} from '../shared/constants'; -export default function useClientLocale(): string { +export default function useLocale(): string { let locale; // The types aren't entirely correct here. Outside of Next.js @@ -12,8 +13,8 @@ export default function useClientLocale(): string { if (typeof params?.[LOCALE_SEGMENT_NAME] === 'string') { locale = params[LOCALE_SEGMENT_NAME]; } else { - // eslint-disable-next-line react-hooks/rules-of-hooks -- Reading from context conditionally is fine - locale = useLocale(); + // eslint-disable-next-line react-hooks/rules-of-hooks -- Reading from context conditionally is fine as long as we're in the render phase + locale = useBaseLocale(); } return locale; diff --git a/packages/next-intl/src/react-server/NextIntlClientProvider.tsx b/packages/next-intl/src/react-server/NextIntlClientProvider.tsx new file mode 100644 index 000000000..53e685eb4 --- /dev/null +++ b/packages/next-intl/src/react-server/NextIntlClientProvider.tsx @@ -0,0 +1,27 @@ +import React, {ComponentProps} from 'react'; +import BaseNextIntlClientProvider from '../shared/NextIntlClientProvider'; +import useLocale from './useLocale'; +import useNow from './useNow'; +import useTimeZone from './useTimeZone'; + +type Props = ComponentProps; + +export default function NextIntlClientProvider({ + locale, + now, + timeZone, + ...rest +}: Props) { + const defaultLocale = useLocale(); + const defaultNow = useNow(); + const defaultTimeZone = useTimeZone(); + + return ( + + ); +} diff --git a/packages/next-intl/src/react-server/getBaseTranslator.tsx b/packages/next-intl/src/react-server/getBaseTranslator.tsx new file mode 100644 index 000000000..708a42e6e --- /dev/null +++ b/packages/next-intl/src/react-server/getBaseTranslator.tsx @@ -0,0 +1,114 @@ +import {ReactElement, ReactNodeArray, cache} from 'react'; +import { + Formats, + TranslationValues, + RichTranslationValues, + MessageKeys, + NamespaceKeys, + NestedKeyOf, + NestedValueOf, + createTranslator, + MarkupTranslationValues +} from 'use-intl/core'; +import getConfig from '../server/getConfig'; + +const getMessageFormatCache = cache(() => new Map()); + +async function getTranslatorImpl< + NestedKey extends NamespaceKeys< + IntlMessages, + NestedKeyOf + > = never +>( + 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; +}> { + const config = await getConfig(locale); + return createTranslator({ + ...config, + messageFormatCache: getMessageFormatCache(), + namespace, + messages: config.messages + }); +} + +export default cache(getTranslatorImpl); diff --git a/packages/next-intl/src/react-server/index.tsx b/packages/next-intl/src/react-server/index.tsx index 30e2b1fc3..59ad379b8 100644 --- a/packages/next-intl/src/react-server/index.tsx +++ b/packages/next-intl/src/react-server/index.tsx @@ -6,24 +6,14 @@ * Make sure this mirrors the API from '../react-client'. */ -export * from 'use-intl/dist/src/core'; -export {default as NextIntlClientProvider} from '../shared/NextIntlClientProvider'; +// Replaced exports from the `react` package +export {default as useLocale} from './useLocale'; +export {default as useTranslations} from './useTranslations'; +export {default as useFormatter} from './useFormatter'; +export {default as useNow} from './useNow'; +export {default as useTimeZone} from './useTimeZone'; +export {default as useMessages} from './useMessages'; +export {default as NextIntlClientProvider} from './NextIntlClientProvider'; -function notSupported() { - throw new Error( - `The React APIs of next-intl are currently not available in Server Components. - -You can try one of these options: -1. Try out the Server Components beta, see https://next-intl-docs.vercel.app/docs/getting-started -2. Use the core library as a stopgap solution, see https://next-intl-docs.vercel.app/docs/environments/core-library -` - ); -} - -export const IntlProvider = notSupported; -export const useTranslations = notSupported; -export const useIntl = notSupported; -export const useLocale = notSupported; -export const useNow = notSupported; -export const useTimeZone = notSupported; -export const Link = notSupported; +// Everything from `core` +export * from 'use-intl/core'; diff --git a/packages/next-intl/src/react-server/useFormatter.tsx b/packages/next-intl/src/react-server/useFormatter.tsx new file mode 100644 index 000000000..4001d0e64 --- /dev/null +++ b/packages/next-intl/src/react-server/useFormatter.tsx @@ -0,0 +1,12 @@ +import type {useFormatter as useFormatterType} from 'use-intl'; +import getFormatter from '../server/getFormatter'; +import useHook from './useHook'; +import useLocale from './useLocale'; + +export default function useFormatter( + // eslint-disable-next-line no-empty-pattern + ...[]: Parameters +): ReturnType { + const locale = useLocale(); + 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 new file mode 100644 index 000000000..e19ceb533 --- /dev/null +++ b/packages/next-intl/src/react-server/useHook.tsx @@ -0,0 +1,22 @@ +import {use} from 'react'; + +export default function useHook( + hookName: string, + promise: Promise +) { + try { + return use(promise); + } catch (error: any) { + if ( + error instanceof TypeError && + error.message.includes("Cannot read properties of null (reading 'use')") + ) { + throw new Error( + `\`${hookName}\` is not callable within an async component. Please refer to https://next-intl-docs.vercel.app/docs/environments/server-client-components#async-components`, + {cause: error} + ); + } else { + throw error; + } + } +} diff --git a/packages/next-intl/src/react-server/useLocale.tsx b/packages/next-intl/src/react-server/useLocale.tsx new file mode 100644 index 000000000..e221e4a34 --- /dev/null +++ b/packages/next-intl/src/react-server/useLocale.tsx @@ -0,0 +1,9 @@ +import type {useLocale as useLocaleType} from 'use-intl'; +import {getRequestLocale} from '../server/RequestLocale'; + +export default function useLocale( + // eslint-disable-next-line no-empty-pattern + ...[]: Parameters +): ReturnType { + return getRequestLocale(); +} diff --git a/packages/next-intl/src/react-server/useMessages.tsx b/packages/next-intl/src/react-server/useMessages.tsx new file mode 100644 index 000000000..bf9282380 --- /dev/null +++ b/packages/next-intl/src/react-server/useMessages.tsx @@ -0,0 +1,12 @@ +import type {useMessages as useMessagesType} from 'use-intl'; +import getMessages from '../server/getMessages'; +import useHook from './useHook'; +import useLocale from './useLocale'; + +export default function useMessages( + // eslint-disable-next-line no-empty-pattern + ...[]: Parameters +): ReturnType { + const locale = useLocale(); + 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 new file mode 100644 index 000000000..289b73c4f --- /dev/null +++ b/packages/next-intl/src/react-server/useNow.tsx @@ -0,0 +1,17 @@ +import type {useNow as useNowType} from 'use-intl'; +import getNow from '../server/getNow'; +import useHook from './useHook'; +import useLocale from './useLocale'; + +export default function useNow( + ...[options]: Parameters +): ReturnType { + if (options?.updateInterval != null) { + console.error( + "`useNow` doesn't support the `updateInterval` option in Server Components, the value will be ignored. If you need the value to update, you can convert the component to a Client Component." + ); + } + + const locale = useLocale(); + 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 new file mode 100644 index 000000000..4b5e4508a --- /dev/null +++ b/packages/next-intl/src/react-server/useTimeZone.tsx @@ -0,0 +1,12 @@ +import type {useTimeZone as useTimeZoneType} from 'use-intl'; +import getTimeZone from '../server/getTimeZone'; +import useHook from './useHook'; +import useLocale from './useLocale'; + +export default function useTimeZone( + // eslint-disable-next-line no-empty-pattern + ...[]: Parameters +): ReturnType { + const locale = useLocale(); + return useHook('useTimeZone', getTimeZone({locale})); +} diff --git a/packages/next-intl/src/react-server/useTranslations.tsx b/packages/next-intl/src/react-server/useTranslations.tsx new file mode 100644 index 000000000..3a39ea91b --- /dev/null +++ b/packages/next-intl/src/react-server/useTranslations.tsx @@ -0,0 +1,17 @@ +import type {useTranslations as useTranslationsType} from 'use-intl'; +import getBaseTranslator from './getBaseTranslator'; +import useHook from './useHook'; +import useLocale from './useLocale'; + +export default function useTranslations( + ...[namespace]: Parameters +): ReturnType { + const locale = useLocale(); + + const result = useHook( + 'useTranslations', + getBaseTranslator(locale, namespace) + ); + + return result; +} diff --git a/packages/next-intl/src/server.tsx b/packages/next-intl/src/server.tsx new file mode 100644 index 000000000..ca39cd0df --- /dev/null +++ b/packages/next-intl/src/server.tsx @@ -0,0 +1 @@ +export * from './server/index'; diff --git a/packages/next-intl/src/server/RequestLocale.tsx b/packages/next-intl/src/server/RequestLocale.tsx new file mode 100644 index 000000000..5ec5e7a30 --- /dev/null +++ b/packages/next-intl/src/server/RequestLocale.tsx @@ -0,0 +1,45 @@ +import {headers} from 'next/headers'; +import {cache} from 'react'; +import {HEADER_LOCALE_NAME} from '../shared/constants'; + +const getLocaleFromHeader = cache(() => { + let locale; + + try { + locale = headers().get(HEADER_LOCALE_NAME); + } catch (error) { + if ( + error instanceof Error && + (error as any).digest === 'DYNAMIC_SERVER_USAGE' + ) { + throw new Error( + 'Usage of next-intl APIs in Server Components currently opts into dynamic rendering. This limitation will eventually be lifted, but as a stopgap solution, you can use the `unstable_setRequestLocale` API to enable static rendering, see https://next-intl-docs.vercel.app/docs/getting-started/app-router-server-components#static-rendering', + {cause: error} + ); + } else { + throw error; + } + } + + if (!locale) { + throw new Error( + `Unable to find \`next-intl\` locale because the middleware didn't run on this request. See https://next-intl-docs.vercel.app/docs/routing/middleware#unable-to-find-locale` + ); + } + + return locale; +}); + +// Workaround until `createServerContext` is available +const getCache = cache(() => { + const value: {locale?: string} = {locale: undefined}; + return value; +}); + +export function setRequestLocale(locale: string) { + getCache().locale = locale; +} + +export function getRequestLocale(): string { + return getCache().locale || getLocaleFromHeader(); +} diff --git a/packages/next-intl/src/server/createRequestConfig.tsx b/packages/next-intl/src/server/createRequestConfig.tsx new file mode 100644 index 000000000..b540895b8 --- /dev/null +++ b/packages/next-intl/src/server/createRequestConfig.tsx @@ -0,0 +1,9 @@ +// @ts-expect-error +// eslint-disable-next-line import/no-extraneous-dependencies +import getRuntimeConfig from 'next-intl/config'; +import type {IntlConfig} from 'use-intl/core'; +import type {GetRequestConfigParams} from './getRequestConfig'; + +export default getRuntimeConfig as ( + params: GetRequestConfigParams +) => IntlConfig | Promise; diff --git a/packages/next-intl/src/server/getConfig.tsx b/packages/next-intl/src/server/getConfig.tsx new file mode 100644 index 000000000..4fb5e2548 --- /dev/null +++ b/packages/next-intl/src/server/getConfig.tsx @@ -0,0 +1,34 @@ +import {cache} from 'react'; +import {initializeConfig} from 'use-intl/core'; +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}); + if (result instanceof Promise) { + result = await result; + } + return { + ...result, + now: result?.now || getDefaultNow(), + timeZone: result?.timeZone || getDefaultTimeZone() + }; + } +); + +const getConfig = cache(async (locale: string) => { + const runtimeConfig = await receiveRuntimeConfig(locale, createRequestConfig); + const opts = {...runtimeConfig, locale}; + return initializeConfig(opts); +}); + +export default getConfig; diff --git a/packages/next-intl/src/server/getFormatter.tsx b/packages/next-intl/src/server/getFormatter.tsx new file mode 100644 index 000000000..ed36f11a6 --- /dev/null +++ b/packages/next-intl/src/server/getFormatter.tsx @@ -0,0 +1,20 @@ +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. + * + * The formatter automatically receives the request config, but + * you can override it by passing in additional options. + */ +export default async function getFormatter(opts?: {locale?: string}) { + const locale = await resolveLocaleArg(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..4aacc4d38 --- /dev/null +++ b/packages/next-intl/src/server/getLocale.tsx @@ -0,0 +1,5 @@ +import {getRequestLocale} from './RequestLocale'; + +export default function getLocale() { + return Promise.resolve(getRequestLocale()); +} diff --git a/packages/next-intl/src/server/getMessages.tsx b/packages/next-intl/src/server/getMessages.tsx new file mode 100644 index 000000000..0ccdf556d --- /dev/null +++ b/packages/next-intl/src/server/getMessages.tsx @@ -0,0 +1,20 @@ +import {cache} from 'react'; +import getConfig from './getConfig'; +import resolveLocaleArg from './resolveLocaleArg'; + +const getMessagesImpl = cache(async (locale: string) => { + const config = await getConfig(locale); + + if (!config.messages) { + throw new Error( + 'No messages found. Have you configured them correctly? See https://next-intl-docs.vercel.app/docs/configuration#messages' + ); + } + + return config.messages; +}); + +export default async function getMessages(opts?: {locale?: string}) { + const locale = await resolveLocaleArg(opts); + return getMessagesImpl(locale); +} diff --git a/packages/next-intl/src/server/getNow.tsx b/packages/next-intl/src/server/getNow.tsx new file mode 100644 index 000000000..868420e62 --- /dev/null +++ b/packages/next-intl/src/server/getNow.tsx @@ -0,0 +1,13 @@ +import {cache} from 'react'; +import getConfig from './getConfig'; +import resolveLocaleArg from './resolveLocaleArg'; + +const getNowImpl = cache(async (locale: string) => { + const config = await getConfig(locale); + return config.now; +}); + +export default async function getNow(opts?: {locale?: string}) { + const locale = await resolveLocaleArg(opts); + return getNowImpl(locale); +} diff --git a/packages/next-intl/src/server/getRequestConfig.tsx b/packages/next-intl/src/server/getRequestConfig.tsx new file mode 100644 index 000000000..e28c54584 --- /dev/null +++ b/packages/next-intl/src/server/getRequestConfig.tsx @@ -0,0 +1,18 @@ +import type {IntlConfig} from 'use-intl/core'; + +type RequestConfig = Omit; + +export type GetRequestConfigParams = { + locale: string; +}; + +/** + * Should be called in `i18n.ts` to create the configuration for the current request. + */ +export default function getRequestConfig( + createRequestConfig: ( + params: GetRequestConfigParams + ) => RequestConfig | Promise +) { + return createRequestConfig; +} diff --git a/packages/next-intl/src/server/getTimeZone.tsx b/packages/next-intl/src/server/getTimeZone.tsx new file mode 100644 index 000000000..862a5e2cf --- /dev/null +++ b/packages/next-intl/src/server/getTimeZone.tsx @@ -0,0 +1,13 @@ +import {cache} from 'react'; +import getConfig from './getConfig'; +import resolveLocaleArg from './resolveLocaleArg'; + +const getTimeZoneImpl = cache(async (locale: string) => { + const config = await getConfig(locale); + return config.timeZone; +}); + +export default async function getTimeZone(opts?: {locale?: string}) { + const locale = await resolveLocaleArg(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..4dfefb85e --- /dev/null +++ b/packages/next-intl/src/server/getTranslations.tsx @@ -0,0 +1,220 @@ +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'; + +// Maintainer note: `getTranslations` has two different call signatures. +// We need to define these with function overloads, otherwise TypeScript +// messes up the return type. + +function getTranslations< + 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?: 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; +}>; +function getTranslations< + NestedKey extends NamespaceKeys< + IntlMessages, + NestedKeyOf + > = never +>(opts?: { + 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; +}>; +async function getTranslations< + NestedKey extends NamespaceKeys< + IntlMessages, + NestedKeyOf + > = never +>(namespaceOrOpts?: NestedKey | {locale: string; namespace?: NestedKey}) { + let namespace: NestedKey | undefined; + let locale: string | undefined; + + if (typeof namespaceOrOpts === 'string') { + namespace = namespaceOrOpts; + } else if (namespaceOrOpts) { + locale = namespaceOrOpts.locale; + namespace = namespaceOrOpts.namespace; + } + + const config = await getConfig(locale || (await getLocale())); + + return createTranslator({ + ...config, + namespace, + messages: config.messages + }); +} + +export default cache(getTranslations); diff --git a/packages/next-intl/src/server/index.tsx b/packages/next-intl/src/server/index.tsx index f04ae0554..3d30e3968 100644 --- a/packages/next-intl/src/server/index.tsx +++ b/packages/next-intl/src/server/index.tsx @@ -2,33 +2,13 @@ * Server-only APIs available via `next-intl/server`. */ -import createMiddleware_ from '../middleware'; -import MiddlewareConfig from '../middleware/NextIntlMiddlewareConfig'; - -let hasWarned = false; -/** @deprecated Should be imported as `import createMiddleware from 'next-intl/middleware', not from `next-intl/server`. */ -export function createIntlMiddleware(config: MiddlewareConfig) { - if (!hasWarned) { - hasWarned = 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 getFormatter} from './getFormatter'; +export {default as getNow} from './getNow'; +export {default as getTimeZone} from './getTimeZone'; +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'; diff --git a/packages/next-intl/src/server/resolveLocaleArg.tsx b/packages/next-intl/src/server/resolveLocaleArg.tsx new file mode 100644 index 000000000..d4828e69f --- /dev/null +++ b/packages/next-intl/src/server/resolveLocaleArg.tsx @@ -0,0 +1,11 @@ +import getLocale from './getLocale'; + +export default function resolveLocaleArg(opts?: { + locale?: string; +}): Promise { + if (opts?.locale) { + return Promise.resolve(opts.locale); + } else { + return getLocale(); + } +} diff --git a/packages/next-intl/src/shared/BaseLink.tsx b/packages/next-intl/src/shared/BaseLinkWithLocale.tsx similarity index 89% rename from packages/next-intl/src/shared/BaseLink.tsx rename to packages/next-intl/src/shared/BaseLinkWithLocale.tsx index 8e3eb63a7..df5d01b41 100644 --- a/packages/next-intl/src/shared/BaseLink.tsx +++ b/packages/next-intl/src/shared/BaseLinkWithLocale.tsx @@ -3,19 +3,22 @@ import NextLink from 'next/link'; import {usePathname} from 'next/navigation'; import React, {ComponentProps, forwardRef, useEffect, useState} from 'react'; -import useClientLocale from '../client/useClientLocale'; +import useLocale from '../react-client/useLocale'; import {isLocalHref, localizeHref, prefixHref} from './utils'; 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; - const defaultLocale = useClientLocale(); + const defaultLocale = useLocale(); const isChangingLocale = locale !== defaultLocale; const [localizedHref, setLocalizedHref] = useState(() => @@ -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 c566bc9ee..2b0385ae4 100644 --- a/packages/next-intl/src/shared/NextIntlClientProvider.tsx +++ b/packages/next-intl/src/shared/NextIntlClientProvider.tsx @@ -1,54 +1,25 @@ 'use client'; -import {useRouter} from 'next/router'; import React, {ComponentProps} from 'react'; -import {IntlProvider} from 'use-intl'; +// Workaround for some bundle splitting until we have ESM +import {IntlProvider} from 'use-intl/_IntlProvider'; -type Props = Omit, 'locale' | 'now'> & { +type Props = Omit, 'locale'> & { + /** This is automatically received when being rendered from a Server Component. In all other cases, e.g. when rendered from a Client Component, a unit test or with the Pages Router, you can pass this prop explicitly. */ locale?: string; - /** If a string is supplied, make sure this conforms to the ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ */ - now?: Date | string; }; -export default function NextIntlClientProvider({ - children, - locale, - now, - ...rest -}: Props) { - let router; - try { - // Reading from context is practically ok to do conditionally - // eslint-disable-next-line react-hooks/rules-of-hooks - router = useRouter(); - } catch (error) { - // Calling `useRouter` is not supported in the app folder - } - - // The router can be undefined if used in a context outside - // of Next.js (e.g. unit tests, Storybook, ...) - if (!locale && router) { - locale = router.locale; - } - - // Currently RSC serialize dates to strings, therefore make sure we have - // a date object. We might be able to remove this once more types have - // first-class serialization support (https://github.com/facebook/react/issues/25687) - if (typeof now === 'string') { - now = new Date(now); - } +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. if (!locale) { throw new Error( process.env.NODE_ENV !== 'production' - ? "Couldn't determine locale. Please pass an explicit `locale` prop the provider, or if you're using the `pages` folder, use internationalized routing (https://nextjs.org/docs/advanced-features/i18n-routing)." + ? 'Failed to determine locale in `NextIntlClientProvider`, please provide the `locale` prop explicitly.\n\nSee https://next-intl-docs.vercel.app/docs/configuration#locale' : undefined ); } - return ( - - {children} - - ); + return ; } diff --git a/packages/next-intl/src/shared/constants.tsx b/packages/next-intl/src/shared/constants.tsx index 2e155721f..e80fad277 100644 --- a/packages/next-intl/src/shared/constants.tsx +++ b/packages/next-intl/src/shared/constants.tsx @@ -2,5 +2,8 @@ // https://nextjs.org/docs/advanced-features/i18n-routing#leveraging-the-next_locale-cookie export const COOKIE_LOCALE_NAME = 'NEXT_LOCALE'; +// Should take precedence over the cookie +export const HEADER_LOCALE_NAME = 'X-NEXT-INTL-LOCALE'; + // In a URL like "/en-US/about", the locale segment is "en-US" export const LOCALE_SEGMENT_NAME = 'locale'; diff --git a/packages/next-intl/src/shared/redirectWithLocale.tsx b/packages/next-intl/src/shared/redirectWithLocale.tsx new file mode 100644 index 000000000..f14eb9e81 --- /dev/null +++ b/packages/next-intl/src/shared/redirectWithLocale.tsx @@ -0,0 +1,12 @@ +import {redirect as nextRedirect} from 'next/navigation'; +import {AllLocales, ParametersExceptFirst} from './types'; +import {localizePathname} from './utils'; + +export default function redirectWithLocale( + pathname: string, + locale: AllLocales[number], + ...args: ParametersExceptFirst +) { + const localizedPathname = localizePathname(locale, pathname); + return nextRedirect(localizedPathname, ...args); +} diff --git a/packages/next-intl/src/shared/types.tsx b/packages/next-intl/src/shared/types.tsx new file mode 100644 index 000000000..0a628c03a --- /dev/null +++ b/packages/next-intl/src/shared/types.tsx @@ -0,0 +1,21 @@ +export type AllLocales = ReadonlyArray; + +export type Pathnames = Record< + string, + {[Key in Locales[number]]: string} | string +>; + +export type ParametersExceptFirst = Fn extends ( + arg0: any, + ...rest: infer R +) => any + ? R + : never; + +export type ParametersExceptFirstTwo = Fn extends ( + arg0: any, + arg1: any, + ...rest: infer R +) => any + ? R + : never; diff --git a/packages/next-intl/src/shared/utils.tsx b/packages/next-intl/src/shared/utils.tsx index a9b780c48..36ae5ab69 100644 --- a/packages/next-intl/src/shared/utils.tsx +++ b/packages/next-intl/src/shared/utils.tsx @@ -78,10 +78,13 @@ export function unlocalizePathname(pathname: string, locale: string) { export function localizePathname(locale: string, pathname: string) { let localizedHref = '/' + locale; - if (pathname !== '/') { - localizedHref += pathname; + // Avoid trailing slashes + if (/^\/(\?.*)?$/.test(pathname)) { + pathname = pathname.slice(1); } + localizedHref += pathname; + return localizedHref; } @@ -89,3 +92,26 @@ export function hasPathnamePrefixed(locale: string, pathname: string) { const prefix = `/${locale}`; return pathname === prefix || pathname.startsWith(`${prefix}/`); } + +export function matchesPathname( + /** E.g. `/users/[userId]-[userName]` */ + template: string, + /** E.g. `/users/23-jane` */ + pathname: string +) { + const regex = templateToRegex(template); + return regex.test(pathname); +} + +export function templateToRegex(template: string): RegExp { + const regexPattern = template + .replace(/\[([^\]]+)\]/g, (match) => { + if (match.startsWith('[...')) return '(.*)'; + if (match.startsWith('[[...')) return '(.*)'; + return '([^/]+)'; + }) + // Clean up regex match remainders from optional catchall ('[[...slug]]') + .replaceAll('(.*)]', '(.*)'); + + return new RegExp(`^${regexPattern}$`); +} diff --git a/packages/next-intl/test/middleware/getAlternateLinksHeaderValue.test.tsx b/packages/next-intl/test/middleware/getAlternateLinksHeaderValue.test.tsx index e29c88e71..c401f382a 100644 --- a/packages/next-intl/test/middleware/getAlternateLinksHeaderValue.test.tsx +++ b/packages/next-intl/test/middleware/getAlternateLinksHeaderValue.test.tsx @@ -4,9 +4,10 @@ import {NextRequest} from 'next/server'; import {it, expect} from 'vitest'; import {MiddlewareConfigWithDefaults} from '../../src/middleware/NextIntlMiddlewareConfig'; import getAlternateLinksHeaderValue from '../../src/middleware/getAlternateLinksHeaderValue'; +import {Pathnames} from '../../src/navigation'; it('works for prefixed routing (as-needed)', () => { - const config: MiddlewareConfigWithDefaults = { + const config: MiddlewareConfigWithDefaults<['en', 'es']> = { defaultLocale: 'en', locales: ['en', 'es'], alternateLinks: true, @@ -15,10 +16,11 @@ it('works for prefixed routing (as-needed)', () => { }; expect( - getAlternateLinksHeaderValue( + getAlternateLinksHeaderValue({ config, - new NextRequest('https://example.com/') - ).split(', ') + request: new NextRequest('https://example.com/'), + resolvedLocale: 'en' + }).split(', ') ).toEqual([ '; rel="alternate"; hreflang="en"', '; rel="alternate"; hreflang="es"', @@ -26,10 +28,11 @@ it('works for prefixed routing (as-needed)', () => { ]); expect( - getAlternateLinksHeaderValue( + getAlternateLinksHeaderValue({ config, - new NextRequest('https://example.com/about') - ).split(', ') + request: new NextRequest('https://example.com/about'), + resolvedLocale: 'en' + }).split(', ') ).toEqual([ '; rel="alternate"; hreflang="en"', '; rel="alternate"; hreflang="es"', @@ -37,8 +40,85 @@ it('works for prefixed routing (as-needed)', () => { ]); }); +it('works for prefixed routing (as-needed) with `pathnames`', () => { + const config: MiddlewareConfigWithDefaults<['en', 'de']> = { + defaultLocale: 'en', + locales: ['en', 'de'], + alternateLinks: true, + localePrefix: 'as-needed', + localeDetection: true + }; + const pathnames = { + '/': '/', + '/about': { + en: '/about', + de: '/ueber' + }, + '/users': { + en: '/users', + de: '/benutzer' + }, + '/users/[userId]': { + en: '/users/[userId]', + de: '/benutzer/[userId]' + } + }; + + expect( + getAlternateLinksHeaderValue({ + config, + request: new NextRequest('https://example.com/'), + resolvedLocale: 'en', + localizedPathnames: pathnames['/'] + }).split(', ') + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + + expect( + getAlternateLinksHeaderValue({ + config, + request: new NextRequest('https://example.com/about'), + resolvedLocale: 'en', + localizedPathnames: pathnames['/about'] + }).split(', ') + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + + expect( + getAlternateLinksHeaderValue({ + config, + request: new NextRequest('https://example.com/de/ueber'), + resolvedLocale: 'de', + localizedPathnames: pathnames['/about'] + }).split(', ') + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + + expect( + getAlternateLinksHeaderValue({ + config, + request: new NextRequest('https://example.com/users/2'), + resolvedLocale: 'en', + localizedPathnames: pathnames['/users/[userId]'] + }).split(', ') + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); +}); + it('works for prefixed routing (always)', () => { - const config: MiddlewareConfigWithDefaults = { + const config: MiddlewareConfigWithDefaults<['en', 'es']> = { defaultLocale: 'en', locales: ['en', 'es'], alternateLinks: true, @@ -47,10 +127,11 @@ it('works for prefixed routing (always)', () => { }; expect( - getAlternateLinksHeaderValue( + getAlternateLinksHeaderValue({ config, - new NextRequest('https://example.com/') - ).split(', ') + request: new NextRequest('https://example.com/'), + resolvedLocale: 'en' + }).split(', ') ).toEqual([ '; rel="alternate"; hreflang="en"', '; rel="alternate"; hreflang="es"', @@ -58,10 +139,11 @@ it('works for prefixed routing (always)', () => { ]); expect( - getAlternateLinksHeaderValue( + getAlternateLinksHeaderValue({ config, - new NextRequest('https://example.com/about') - ).split(', ') + request: new NextRequest('https://example.com/about'), + resolvedLocale: 'en' + }).split(', ') ).toEqual([ '; rel="alternate"; hreflang="en"', '; rel="alternate"; hreflang="es"', @@ -70,7 +152,7 @@ it('works for prefixed routing (always)', () => { }); it("works for type domain with `localePrefix: 'as-needed'`", () => { - const config: MiddlewareConfigWithDefaults = { + const config: MiddlewareConfigWithDefaults<['en', 'es', 'fr']> = { defaultLocale: 'en', locales: ['en', 'es', 'fr'], alternateLinks: true, @@ -96,14 +178,16 @@ it("works for type domain with `localePrefix: 'as-needed'`", () => { }; [ - getAlternateLinksHeaderValue( + getAlternateLinksHeaderValue({ config, - new NextRequest('https://example.com/') - ).split(', '), - getAlternateLinksHeaderValue( + request: new NextRequest('https://example.com/'), + resolvedLocale: 'en' + }).split(', '), + getAlternateLinksHeaderValue({ config, - new NextRequest('https://example.es/') - ).split(', ') + request: new NextRequest('https://example.es'), + resolvedLocale: 'es' + }).split(', ') ].forEach((links) => { expect(links).toEqual([ '; rel="alternate"; hreflang="en"', @@ -116,10 +200,11 @@ it("works for type domain with `localePrefix: 'as-needed'`", () => { }); expect( - getAlternateLinksHeaderValue( + getAlternateLinksHeaderValue({ config, - new NextRequest('https://example.com/about') - ).split(', ') + request: new NextRequest('https://example.com/about'), + resolvedLocale: 'en' + }).split(', ') ).toEqual([ '; rel="alternate"; hreflang="en"', '; rel="alternate"; hreflang="en"', @@ -131,7 +216,7 @@ it("works for type domain with `localePrefix: 'as-needed'`", () => { }); it("works for type domain with `localePrefix: 'always'`", () => { - const config: MiddlewareConfigWithDefaults = { + const config: MiddlewareConfigWithDefaults<['en', 'es', 'fr']> = { defaultLocale: 'en', locales: ['en', 'es', 'fr'], alternateLinks: true, @@ -157,14 +242,16 @@ it("works for type domain with `localePrefix: 'always'`", () => { }; [ - getAlternateLinksHeaderValue( + getAlternateLinksHeaderValue({ config, - new NextRequest('https://example.com/') - ).split(', '), - getAlternateLinksHeaderValue( + request: new NextRequest('https://example.com/'), + resolvedLocale: 'en' + }).split(', '), + getAlternateLinksHeaderValue({ config, - new NextRequest('https://example.es/') - ).split(', ') + request: new NextRequest('https://example.es'), + resolvedLocale: 'es' + }).split(', ') ].forEach((links) => { expect(links).toEqual([ '; rel="alternate"; hreflang="en"', @@ -177,10 +264,11 @@ it("works for type domain with `localePrefix: 'always'`", () => { }); expect( - getAlternateLinksHeaderValue( + getAlternateLinksHeaderValue({ config, - new NextRequest('https://example.com/about') - ).split(', ') + request: new NextRequest('https://example.com/about'), + resolvedLocale: 'en' + }).split(', ') ).toEqual([ '; rel="alternate"; hreflang="en"', '; rel="alternate"; hreflang="en"', @@ -191,8 +279,158 @@ it("works for type domain with `localePrefix: 'always'`", () => { ]); }); +it("works for type domain with `localePrefix: 'as-needed' with `pathnames``", () => { + const config: MiddlewareConfigWithDefaults<['en', 'fr']> = { + alternateLinks: true, + localePrefix: 'as-needed', + localeDetection: true, + defaultLocale: 'en', + locales: ['en', 'fr'], + domains: [ + {defaultLocale: 'en', domain: 'en.example.com', locales: ['en']}, + { + defaultLocale: 'en', + domain: 'ca.example.com', + locales: ['en', 'fr'] + }, + {defaultLocale: 'fr', domain: 'fr.example.com', locales: ['fr']} + ], + pathnames: { + '/': '/', + '/about': { + en: '/about', + fr: '/a-propos' + }, + '/users': { + en: '/users', + fr: '/utilisateurs' + }, + '/users/[userId]': { + en: '/users/[userId]', + fr: '/utilisateurs/[userId]' + }, + '/news/[articleSlug]-[articleId]': { + en: '/news/[articleSlug]-[articleId]', + fr: '/nouvelles/[articleSlug]-[articleId]' + }, + '/products/[...slug]': { + en: '/products/[...slug]', + fr: '/produits/[...slug]' + }, + '/categories/[[...slug]]': { + en: '/categories/[[...slug]]', + fr: '/categories/[[...slug]]' + } + } satisfies Pathnames> + }; + + [ + getAlternateLinksHeaderValue({ + config, + request: new NextRequest('https://en.example.com/'), + resolvedLocale: 'en' + }), + getAlternateLinksHeaderValue({ + config, + request: new NextRequest('https://ca.example.com'), + resolvedLocale: 'en' + }), + getAlternateLinksHeaderValue({ + config, + request: new NextRequest('https://ca.example.com/fr'), + resolvedLocale: 'fr' + }), + getAlternateLinksHeaderValue({ + config, + request: new NextRequest('https://fr.example.com'), + resolvedLocale: 'fr' + }) + ] + .map((links) => links.split(', ')) + .forEach((links) => { + expect(links).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="fr"', + '; rel="alternate"; hreflang="fr"' + ]); + }); + + [ + getAlternateLinksHeaderValue({ + config, + request: new NextRequest('https://en.example.com/about'), + resolvedLocale: 'en', + localizedPathnames: config.pathnames!['/about'] + }), + getAlternateLinksHeaderValue({ + config, + request: new NextRequest('https://ca.example.com/about'), + resolvedLocale: 'en', + localizedPathnames: config.pathnames!['/about'] + }), + getAlternateLinksHeaderValue({ + config, + request: new NextRequest('https://ca.example.com/fr/a-propos'), + resolvedLocale: 'fr', + localizedPathnames: config.pathnames!['/about'] + }), + getAlternateLinksHeaderValue({ + config, + request: new NextRequest('https://fr.example.com/a-propos'), + resolvedLocale: 'fr', + localizedPathnames: config.pathnames!['/about'] + }) + ] + .map((links) => links.split(', ')) + .forEach((links) => { + expect(links).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="fr"', + '; rel="alternate"; hreflang="fr"' + ]); + }); + + [ + getAlternateLinksHeaderValue({ + config, + request: new NextRequest('https://en.example.com/users/42'), + resolvedLocale: 'en', + localizedPathnames: config.pathnames!['/users/[userId]'] + }), + getAlternateLinksHeaderValue({ + config, + request: new NextRequest('https://ca.example.com/users/42'), + resolvedLocale: 'en', + localizedPathnames: config.pathnames!['/users/[userId]'] + }), + getAlternateLinksHeaderValue({ + config, + request: new NextRequest('https://ca.example.com/fr/utilisateurs/42'), + resolvedLocale: 'fr', + localizedPathnames: config.pathnames!['/users/[userId]'] + }), + getAlternateLinksHeaderValue({ + config, + request: new NextRequest('https://fr.example.com/utilisateurs/42'), + resolvedLocale: 'fr', + localizedPathnames: config.pathnames!['/users/[userId]'] + }) + ] + .map((links) => links.split(', ')) + .forEach((links) => { + expect(links).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="fr"', + '; rel="alternate"; hreflang="fr"' + ]); + }); +}); + it('uses the external host name from headers instead of the url of the incoming request (relevant when running the app behind a proxy)', () => { - const config: MiddlewareConfigWithDefaults = { + const config: MiddlewareConfigWithDefaults<['en', 'es']> = { defaultLocale: 'en', locales: ['en', 'es'], alternateLinks: true, @@ -201,16 +439,17 @@ it('uses the external host name from headers instead of the url of the incoming }; expect( - getAlternateLinksHeaderValue( + getAlternateLinksHeaderValue({ config, - new NextRequest('http://127.0.0.1/about', { + request: new NextRequest('http://127.0.0.1/about', { headers: { host: 'example.com', 'x-forwarded-host': 'example.com', 'x-forwarded-proto': 'https' } - }) - ).split(', ') + }), + resolvedLocale: 'en' + }).split(', ') ).toEqual([ '; rel="alternate"; hreflang="en"', '; rel="alternate"; hreflang="es"', @@ -219,7 +458,7 @@ it('uses the external host name from headers instead of the url of the incoming }); it('keeps the port of an external host if provided', () => { - const config: MiddlewareConfigWithDefaults = { + const config: MiddlewareConfigWithDefaults<['en', 'es']> = { defaultLocale: 'en', locales: ['en', 'es'], alternateLinks: true, @@ -228,16 +467,17 @@ it('keeps the port of an external host if provided', () => { }; expect( - getAlternateLinksHeaderValue( + getAlternateLinksHeaderValue({ config, - new NextRequest('http://127.0.0.1/about', { + request: new NextRequest('http://127.0.0.1/about', { headers: { host: 'example.com:3000', 'x-forwarded-host': 'example.com:3000', 'x-forwarded-proto': 'https' } - }) - ).split(', ') + }), + resolvedLocale: 'en' + }).split(', ') ).toEqual([ '; rel="alternate"; hreflang="en"', '; rel="alternate"; hreflang="es"', @@ -246,7 +486,7 @@ it('keeps the port of an external host if provided', () => { }); it('uses the external host name and the port from headers instead of the url with port of the incoming request (relevant when running the app behind a proxy)', () => { - const config: MiddlewareConfigWithDefaults = { + const config: MiddlewareConfigWithDefaults<['en', 'es']> = { defaultLocale: 'en', locales: ['en', 'es'], alternateLinks: true, @@ -255,16 +495,17 @@ it('uses the external host name and the port from headers instead of the url wit }; expect( - getAlternateLinksHeaderValue( + getAlternateLinksHeaderValue({ config, - new NextRequest('http://127.0.0.1:3000/about', { + request: new NextRequest('http://127.0.0.1:3000/about', { headers: { host: 'example.com', 'x-forwarded-host': 'example.com', 'x-forwarded-proto': 'https' } - }) - ).split(', ') + }), + resolvedLocale: 'en' + }).split(', ') ).toEqual([ '; rel="alternate"; hreflang="en"', '; rel="alternate"; hreflang="es"', diff --git a/packages/next-intl/test/middleware/middleware.test.tsx b/packages/next-intl/test/middleware/middleware.test.tsx index 4ece9ec5b..1a1dcd8d9 100644 --- a/packages/next-intl/test/middleware/middleware.test.tsx +++ b/packages/next-intl/test/middleware/middleware.test.tsx @@ -5,10 +5,12 @@ import {NextRequest, NextResponse} from 'next/server'; import {pathToRegexp} from 'path-to-regexp'; import {it, describe, vi, beforeEach, expect, Mock} from 'vitest'; import createIntlMiddleware from '../../src/middleware'; +import {Pathnames} from '../../src/navigation'; import {COOKIE_LOCALE_NAME} from '../../src/shared/constants'; -vi.mock('next/server', () => { - type MiddlewareResponseInit = Parameters[0]; +vi.mock('next/server', async (importActual) => { + const ActualNextServer = (await importActual()) as any; + type MiddlewareResponseInit = Parameters<(typeof NextResponse)['next']>[0]; function createResponse(init: MiddlewareResponseInit) { const response = new Response(null, init); @@ -18,6 +20,7 @@ vi.mock('next/server', () => { return response as NextResponse; } return { + ...ActualNextServer, NextResponse: { next: vi.fn((init: ResponseInit) => createResponse(init)), rewrite: vi.fn((_destination: string, init: ResponseInit) => @@ -32,13 +35,13 @@ vi.mock('next/server', () => { function createMockRequest( pathnameWithSearch = '/', - locale = 'en', + acceptLanguageLocale = 'en', host = 'http://localhost:3000', localeCookieValue?: string, customHeaders?: HeadersInit ) { const headers = new Headers({ - 'accept-language': `${locale};q=0.9,en;q=0.8`, + 'accept-language': `${acceptLanguageLocale};q=0.9,en;q=0.8`, host: new URL(host).host, ...(localeCookieValue && { cookie: `${COOKIE_LOCALE_NAME}=${localeCookieValue}` @@ -46,25 +49,13 @@ function createMockRequest( ...customHeaders }); const url = host + pathnameWithSearch; - - return { - headers, - cookies: new RequestCookies(headers), - url, - nextUrl: { - pathname: pathnameWithSearch.replace(/\?.*$/, ''), - href: url, - search: pathnameWithSearch.includes('?') - ? '?' + pathnameWithSearch.split('?')[1] - : '' - } - } as NextRequest; + return new NextRequest(url, {headers}); } const MockedNextResponse = NextResponse as unknown as { - next: Mock>; - rewrite: Mock>; - redirect: Mock>; + next: Mock>; + rewrite: Mock>; + redirect: Mock>; }; beforeEach(() => { @@ -128,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', () => { @@ -158,21 +150,21 @@ describe('prefix-based routing', () => { ); }); - it('handles hashes for the default locale', () => { - middleware(createMockRequest('/#asdf')); + it('redirects requests for the default locale when prefixed at the root', () => { + middleware(createMockRequest('/en')); expect(MockedNextResponse.next).not.toHaveBeenCalled(); - expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( - 'http://localhost:3000/en/#asdf' + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/' ); }); - it('redirects requests for the default locale when prefixed at the root', () => { - middleware(createMockRequest('/en')); + it('redirects requests for the default locale when prefixed at the root with search params', () => { + middleware(createMockRequest('/en?search')); expect(MockedNextResponse.next).not.toHaveBeenCalled(); expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( - 'http://localhost:3000/' + 'http://localhost:3000/?search' ); }); @@ -205,30 +197,42 @@ describe('prefix-based routing', () => { it('serves requests for other locales when prefixed', () => { middleware(createMockRequest('/de')); - expect(MockedNextResponse.next).toHaveBeenCalled(); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/de' + ); }); it('serves requests for other locales when prefixed with a trailing slash', () => { middleware(createMockRequest('/de/')); - expect(MockedNextResponse.next).toHaveBeenCalled(); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/de/' + ); }); it('serves requests for other locales with query params at the root', () => { middleware(createMockRequest('/de?sort=asc')); - expect(MockedNextResponse.next).toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/de?sort=asc' + ); }); it('serves requests for other locales with query params at a nested path', () => { middleware(createMockRequest('/de/list?sort=asc')); - expect(MockedNextResponse.next).toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/de/list?sort=asc' + ); }); it('sets a cookie', () => { @@ -258,8 +262,11 @@ describe('prefix-based routing', () => { 'x-test': 'test' }) ); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); expect( - MockedNextResponse.next.mock.calls[0][0]?.request?.headers?.get( + MockedNextResponse.rewrite.mock.calls[0][1]?.request?.headers?.get( 'x-test' ) ).toBe('test'); @@ -275,6 +282,288 @@ describe('prefix-based routing', () => { ].join(', ') ); }); + + it('always provides the locale via a request header, even if a cookie exists with the correct value (see https://github.com/amannn/next-intl/discussions/446)', () => { + middleware(createMockRequest('/', 'en', 'http://localhost:3000', 'en')); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect( + MockedNextResponse.rewrite.mock.calls[0][1]?.request?.headers?.get( + 'x-next-intl-locale' + ) + ).toBe('en'); + }); + + describe('localized pathnames', () => { + const middlewareWithPathnames = createIntlMiddleware({ + defaultLocale: 'en', + locales: ['en', 'de'], + localePrefix: 'as-needed', + pathnames: { + '/': '/', + '/about': { + en: '/about', + de: '/ueber' + }, + '/users': { + en: '/users', + de: '/benutzer' + }, + '/users/[userId]': { + en: '/users/[userId]', + de: '/benutzer/[userId]' + }, + '/news/[articleSlug]-[articleId]': { + en: '/news/[articleSlug]-[articleId]', + de: '/neuigkeiten/[articleSlug]-[articleId]' + }, + '/products/[...slug]': { + en: '/products/[...slug]', + de: '/produkte/[...slug]' + }, + '/categories/[[...slug]]': { + en: '/categories/[[...slug]]', + de: '/kategorien/[[...slug]]' + } + } satisfies Pathnames> + }); + + it('serves requests for the default locale at the root', () => { + middlewareWithPathnames(createMockRequest('/', 'en')); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/en' + ); + }); + + it('serves requests for the default locale at nested paths', () => { + middlewareWithPathnames(createMockRequest('/about', 'en')); + middlewareWithPathnames(createMockRequest('/users', 'en')); + middlewareWithPathnames(createMockRequest('/users/1', 'en')); + middlewareWithPathnames( + createMockRequest('/news/happy-newyear-g5b116754', 'en') + ); + middlewareWithPathnames( + createMockRequest('/products/apparel/t-shirts', 'en') + ); + middlewareWithPathnames( + createMockRequest('/categories/women/t-shirts', 'en') + ); + + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalledTimes(6); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/en/about' + ); + expect(MockedNextResponse.rewrite.mock.calls[1][0].toString()).toBe( + 'http://localhost:3000/en/users' + ); + expect(MockedNextResponse.rewrite.mock.calls[2][0].toString()).toBe( + 'http://localhost:3000/en/users/1' + ); + expect(MockedNextResponse.rewrite.mock.calls[3][0].toString()).toBe( + 'http://localhost:3000/en/news/happy-newyear-g5b116754' + ); + expect(MockedNextResponse.rewrite.mock.calls[4][0].toString()).toBe( + 'http://localhost:3000/en/products/apparel/t-shirts' + ); + expect(MockedNextResponse.rewrite.mock.calls[5][0].toString()).toBe( + 'http://localhost:3000/en/categories/women/t-shirts' + ); + }); + + it('serves requests for a non-default locale at the root', () => { + middlewareWithPathnames(createMockRequest('/de', 'de')); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); // We rewrite just in case + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/de' + ); + }); + + it('serves requests for a non-default locale at nested paths', () => { + middlewareWithPathnames(createMockRequest('/de/ueber', 'de')); + middlewareWithPathnames(createMockRequest('/de/benutzer', 'de')); + middlewareWithPathnames(createMockRequest('/de/benutzer/1', 'de')); + middlewareWithPathnames( + createMockRequest('/de/neuigkeiten/happy-newyear-g5b116754', 'de') + ); + middlewareWithPathnames( + createMockRequest('/de/produkte/kleidung/t-shirts', 'de') + ); + middlewareWithPathnames( + createMockRequest('/de/kategorien/frauen/t-shirts', 'de') + ); + + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalledTimes(6); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/de/about' + ); + expect(MockedNextResponse.rewrite.mock.calls[1][0].toString()).toBe( + 'http://localhost:3000/de/users' + ); + expect(MockedNextResponse.rewrite.mock.calls[2][0].toString()).toBe( + 'http://localhost:3000/de/users/1' + ); + expect(MockedNextResponse.rewrite.mock.calls[3][0].toString()).toBe( + 'http://localhost:3000/de/news/happy-newyear-g5b116754' + ); + expect(MockedNextResponse.rewrite.mock.calls[4][0].toString()).toBe( + 'http://localhost:3000/de/products/kleidung/t-shirts' + ); + expect(MockedNextResponse.rewrite.mock.calls[5][0].toString()).toBe( + 'http://localhost:3000/de/categories/frauen/t-shirts' + ); + }); + + it('redirects a request for a localized route that is not associated with the requested locale', () => { + middlewareWithPathnames(createMockRequest('/ueber', 'en')); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).toHaveBeenCalledTimes(1); + expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/about' + ); + }); + + it('redirects when a pathname from the default locale ends up with a different locale', () => { + // Relevant to avoid duplicate content issues + middlewareWithPathnames(createMockRequest('/de/about', 'de')); + middlewareWithPathnames(createMockRequest('/de/users/2', 'de')); + middlewareWithPathnames(createMockRequest('/de/users/2?page=1', 'de')); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).toHaveBeenCalledTimes(3); + expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/de/ueber' + ); + expect(MockedNextResponse.redirect.mock.calls[1][0].toString()).toBe( + 'http://localhost:3000/de/benutzer/2' + ); + // + expect(MockedNextResponse.redirect.mock.calls[2][0].toString()).toBe( + 'http://localhost:3000/de/benutzer/2?page=1' + ); + }); + + it('redirects a non-prefixed nested path to a localized alternative if another locale was detected', () => { + middlewareWithPathnames(createMockRequest('/about', 'de')); + middlewareWithPathnames(createMockRequest('/users/2', 'de')); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).toHaveBeenCalledTimes(2); + expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/de/ueber' + ); + expect(MockedNextResponse.redirect.mock.calls[1][0].toString()).toBe( + 'http://localhost:3000/de/benutzer/2' + ); + }); + + it('sets alternate links', () => { + function getLinks(request: NextRequest) { + return middlewareWithPathnames(request) + .headers.get('link') + ?.split(', '); + } + + expect(getLinks(createMockRequest('/', 'en'))).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect(getLinks(createMockRequest('/de', 'de'))).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect(getLinks(createMockRequest('/about', 'en'))).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect(getLinks(createMockRequest('/de/ueber', 'de'))).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect(getLinks(createMockRequest('/users/1', 'en'))).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect(getLinks(createMockRequest('/de/benutzer/1', 'de'))).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect( + getLinks(createMockRequest('/products/apparel/t-shirts', 'en')) + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect( + getLinks(createMockRequest('/de/produkte/apparel/t-shirts', 'de')) + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect(getLinks(createMockRequest('/unknown', 'en'))).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect(getLinks(createMockRequest('/de/unknown', 'de'))).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + }); + + it('rewrites requests when the pathname is mapped for the default locale as well', () => { + const callMiddleware = createIntlMiddleware({ + defaultLocale: 'en', + locales: ['en', 'de'], + localePrefix: 'as-needed', + pathnames: { + '/a': { + en: '/one', + de: '/eins' + }, + '/b/[param]': { + en: '/two/[param]', + de: '/zwei/[param]' + } + } + }); + callMiddleware(createMockRequest('/one', 'en')); + callMiddleware(createMockRequest('/de/eins', 'de')); + callMiddleware(createMockRequest('/two/2', 'en')); + callMiddleware(createMockRequest('/de/zwei/2', 'de')); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalledTimes(4); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/en/a' + ); + expect(MockedNextResponse.rewrite.mock.calls[1][0].toString()).toBe( + 'http://localhost:3000/de/a' + ); + expect(MockedNextResponse.rewrite.mock.calls[2][0].toString()).toBe( + 'http://localhost:3000/en/b/2' + ); + expect(MockedNextResponse.rewrite.mock.calls[3][0].toString()).toBe( + 'http://localhost:3000/de/b/2' + ); + }); + }); }); describe('localePrefix: as-needed, localeDetection: false', () => { @@ -351,16 +640,193 @@ describe('prefix-based routing', () => { it('serves requests for the default locale', () => { middleware(createMockRequest('/en')); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.next).toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/en' + ); }); it('serves requests for non-default locales', () => { middleware(createMockRequest('/de')); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.next).toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/de' + ); + }); + + describe('localized pathnames', () => { + const middlewareWithPathnames = createIntlMiddleware({ + defaultLocale: 'en', + locales: ['en', 'de'], + localePrefix: 'always', + pathnames: { + '/': '/', + '/about': { + en: '/about', + de: '/ueber' + }, + '/users': { + en: '/users', + de: '/benutzer' + }, + '/users/[userId]': { + en: '/users/[userId]', + de: '/benutzer/[userId]' + }, + '/news/[articleSlug]-[articleId]': { + en: '/news/[articleSlug]-[articleId]', + de: '/neuigkeiten/[articleSlug]-[articleId]' + }, + '/products/[...slug]': { + en: '/products/[...slug]', + de: '/produkte/[...slug]' + } + } satisfies Pathnames> + }); + + it('serves requests for the default locale at the root', () => { + middlewareWithPathnames(createMockRequest('/en', 'en')); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/en' + ); + }); + + it('serves requests for the default locale at nested paths', () => { + middlewareWithPathnames(createMockRequest('/en/about', 'en')); + middlewareWithPathnames(createMockRequest('/en/users', 'en')); + middlewareWithPathnames(createMockRequest('/en/users/1', 'en')); + middlewareWithPathnames( + createMockRequest('/en/news/happy-newyear-g5b116754', 'en') + ); + + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalledTimes(4); + expect( + MockedNextResponse.rewrite.mock.calls.map((call) => + call[0].toString() + ) + ).toEqual([ + 'http://localhost:3000/en/about', + 'http://localhost:3000/en/users', + 'http://localhost:3000/en/users/1', + 'http://localhost:3000/en/news/happy-newyear-g5b116754' + ]); + }); + + it('serves requests for a non-default locale at the root', () => { + middlewareWithPathnames(createMockRequest('/de', 'de')); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/de' + ); + }); + + it('serves requests for a non-default locale at nested paths', () => { + middlewareWithPathnames(createMockRequest('/de/ueber', 'de')); + middlewareWithPathnames(createMockRequest('/de/benutzer', 'de')); + middlewareWithPathnames(createMockRequest('/de/benutzer/1', 'de')); + middlewareWithPathnames( + createMockRequest('/de/neuigkeiten/gutes-neues-jahr-g5b116754', 'de') + ); + + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/de/about' + ); + expect(MockedNextResponse.rewrite.mock.calls[1][0].toString()).toBe( + 'http://localhost:3000/de/users' + ); + expect(MockedNextResponse.rewrite.mock.calls[2][0].toString()).toBe( + 'http://localhost:3000/de/users/1' + ); + expect(MockedNextResponse.rewrite.mock.calls[3][0].toString()).toBe( + 'http://localhost:3000/de/news/gutes-neues-jahr-g5b116754' + ); + }); + + it('redirects a request for a localized route that is not associated with the requested locale', () => { + // Relevant to avoid duplicate content issues + middlewareWithPathnames(createMockRequest('/en/ueber', 'en')); + middlewareWithPathnames(createMockRequest('/en/benutzer/12', 'en')); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).toHaveBeenCalledTimes(2); + expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/en/about' + ); + expect(MockedNextResponse.redirect.mock.calls[1][0].toString()).toBe( + 'http://localhost:3000/en/users/12' + ); + }); + + it('sets alternate links', () => { + function getLinks(request: NextRequest) { + return middlewareWithPathnames(request) + .headers.get('link') + ?.split(', '); + } + + expect(getLinks(createMockRequest('/en', 'en'))).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect(getLinks(createMockRequest('/de', 'de'))).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect(getLinks(createMockRequest('/en/about', 'en'))).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect(getLinks(createMockRequest('/de/ueber', 'de'))).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect(getLinks(createMockRequest('/en/users/1', 'en'))).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect(getLinks(createMockRequest('/de/benutzer/1', 'de'))).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect( + getLinks(createMockRequest('/en/products/apparel/t-shirts', 'en')) + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect( + getLinks(createMockRequest('/de/produkte/apparel/t-shirts', 'de')) + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + expect(getLinks(createMockRequest('/en/unknown', 'en'))).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="de"', + '; rel="alternate"; hreflang="x-default"' + ]); + }); }); }); @@ -416,6 +882,19 @@ describe('prefix-based routing', () => { ); }); + it('keeps search params when removing the locale via a redirect', () => { + middleware(createMockRequest('/en?test=1')); + middleware(createMockRequest('/en/about?test=1')); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/?test=1' + ); + expect(MockedNextResponse.redirect.mock.calls[1][0].toString()).toBe( + 'http://localhost:3000/about?test=1' + ); + }); + it('redirects requests with other locales in the path', () => { middleware(createMockRequest('/de', 'de')); expect(MockedNextResponse.next).not.toHaveBeenCalled(); @@ -526,6 +1005,119 @@ describe('prefix-based routing', () => { const response = middleware(createMockRequest('/')); expect(response.headers.get('link')).toBe(null); }); + + describe('localized pathnames', () => { + const middlewareWithPathnames = createIntlMiddleware({ + defaultLocale: 'en', + locales: ['en', 'de'], + localePrefix: 'never', + pathnames: { + '/': '/', + '/about': { + en: '/about', + de: '/ueber' + }, + '/users': { + en: '/users', + de: '/benutzer' + }, + '/users/[userId]': { + en: '/users/[userId]', + de: '/benutzer/[userId]' + }, + '/news/[articleSlug]-[articleId]': { + en: '/news/[articleSlug]-[articleId]', + de: '/neuigkeiten/[articleSlug]-[articleId]' + }, + '/products/[...slug]': { + en: '/products/[...slug]', + de: '/produkte/[...slug]' + } + } satisfies Pathnames> + }); + + it('serves requests for the default locale at the root', () => { + middlewareWithPathnames(createMockRequest('/', 'en')); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/en' + ); + }); + + it('serves requests for the default locale at nested paths', () => { + middlewareWithPathnames(createMockRequest('/about', 'en')); + middlewareWithPathnames(createMockRequest('/users', 'en')); + middlewareWithPathnames(createMockRequest('/users/1', 'en')); + middlewareWithPathnames( + createMockRequest('/news/happy-newyear-g5b116754', 'en') + ); + + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalledTimes(4); + expect( + MockedNextResponse.rewrite.mock.calls.map((call) => + call[0].toString() + ) + ).toEqual([ + 'http://localhost:3000/en/about', + 'http://localhost:3000/en/users', + 'http://localhost:3000/en/users/1', + 'http://localhost:3000/en/news/happy-newyear-g5b116754' + ]); + }); + + it('serves requests for a non-default locale at the root', () => { + middlewareWithPathnames(createMockRequest('/', 'de')); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/de' + ); + }); + + it('serves requests for a non-default locale at nested paths', () => { + middlewareWithPathnames(createMockRequest('/ueber', 'de')); + middlewareWithPathnames(createMockRequest('/benutzer', 'de')); + middlewareWithPathnames(createMockRequest('/benutzer/1', 'de')); + middlewareWithPathnames( + createMockRequest('/neuigkeiten/gutes-neues-jahr-g5b116754', 'de') + ); + + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/de/about' + ); + expect(MockedNextResponse.rewrite.mock.calls[1][0].toString()).toBe( + 'http://localhost:3000/de/users' + ); + expect(MockedNextResponse.rewrite.mock.calls[2][0].toString()).toBe( + 'http://localhost:3000/de/users/1' + ); + expect(MockedNextResponse.rewrite.mock.calls[3][0].toString()).toBe( + 'http://localhost:3000/de/news/gutes-neues-jahr-g5b116754' + ); + }); + + it('redirects a request for a localized route that is not associated with the requested locale', () => { + // Relevant to avoid duplicate content issues + middlewareWithPathnames(createMockRequest('/en/ueber', 'en')); + middlewareWithPathnames(createMockRequest('/en/benutzer/12', 'en')); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).toHaveBeenCalledTimes(2); + expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/en/about' + ); + expect(MockedNextResponse.redirect.mock.calls[1][0].toString()).toBe( + 'http://localhost:3000/en/users/12' + ); + }); + }); }); }); @@ -534,6 +1126,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']}, { @@ -575,22 +1168,31 @@ describe('domain-based routing', () => { it('serves requests for non-default locales at the locale root', () => { middleware(createMockRequest('/fr', 'fr', 'http://ca.example.com')); expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); - expect(MockedNextResponse.next).toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://ca.example.com/fr' + ); }); it('serves requests for non-default locales at the locale root when the accept-language header points to the default locale', () => { middleware(createMockRequest('/fr', 'en', 'http://ca.example.com')); expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); - expect(MockedNextResponse.next).toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://ca.example.com/fr' + ); }); it('serves requests for non-default locales at sub paths', () => { middleware(createMockRequest('/fr/about', 'fr', 'http://ca.example.com')); expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); - expect(MockedNextResponse.next).toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://ca.example.com/fr/about' + ); }); it('returns alternate links', () => { @@ -626,16 +1228,22 @@ describe('domain-based routing', () => { it('serves requests for unknown hosts and non-default locales at the locale root', () => { middleware(createMockRequest('/fr', 'fr', 'http://localhost')); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.next).toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost/fr' + ); }); it('serves requests for unknown hosts and non-default locales at sub paths', () => { middleware(createMockRequest('/fr/about', 'fr', 'http://localhost')); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.next).toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost/fr/about' + ); }); }); @@ -688,8 +1296,11 @@ describe('domain-based routing', () => { it('serves requests for non-default locales at the locale root', () => { middleware(createMockRequest('/fr', 'fr', 'http://ca.example.com')); expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); - expect(MockedNextResponse.next).toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://ca.example.com/fr' + ); }); it('serves requests for non-default locales at sub paths', () => { @@ -697,8 +1308,11 @@ describe('domain-based routing', () => { createMockRequest('/fr/about', 'fr', 'http://ca.example.com') ); expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); - expect(MockedNextResponse.next).toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://ca.example.com/fr/about' + ); }); }); @@ -747,6 +1361,365 @@ describe('domain-based routing', () => { ); }); }); + + describe('localized pathnames', () => { + const middlewareWithPathnames = createIntlMiddleware({ + defaultLocale: 'en', + locales: ['en', 'fr'], + localePrefix: 'as-needed', + domains: [ + {defaultLocale: 'en', domain: 'en.example.com', locales: ['en']}, + { + defaultLocale: 'en', + domain: 'ca.example.com', + locales: ['en', 'fr'] + }, + {defaultLocale: 'fr', domain: 'fr.example.com', locales: ['fr']} + ], + pathnames: { + '/': '/', + '/about': { + en: '/about', + fr: '/a-propos' + }, + '/users': { + en: '/users', + fr: '/utilisateurs' + }, + '/users/[userId]': { + en: '/users/[userId]', + fr: '/utilisateurs/[userId]' + }, + '/news/[articleSlug]-[articleId]': { + en: '/news/[articleSlug]-[articleId]', + fr: '/nouvelles/[articleSlug]-[articleId]' + }, + '/products/[...slug]': { + en: '/products/[...slug]', + fr: '/produits/[...slug]' + }, + '/categories/[[...slug]]': { + en: '/categories/[[...slug]]', + fr: '/categories/[[...slug]]' + } + } satisfies Pathnames> + }); + + it('serves requests for the default locale at the root', () => { + middlewareWithPathnames( + createMockRequest('/', 'en', 'http://en.example.com') + ); + middlewareWithPathnames( + createMockRequest('/', 'en', 'http://ca.example.com') + ); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalledTimes(2); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://en.example.com/en' + ); + expect(MockedNextResponse.rewrite.mock.calls[1][0].toString()).toBe( + 'http://ca.example.com/en' + ); + }); + + it('serves requests for the default locale at nested paths', () => { + middlewareWithPathnames( + createMockRequest('/about', 'en', 'http://en.example.com') + ); + middlewareWithPathnames( + createMockRequest('/users', 'en', 'http://en.example.com') + ); + middlewareWithPathnames( + createMockRequest('/users/1', 'en', 'http://en.example.com') + ); + middlewareWithPathnames( + createMockRequest( + '/news/happy-newyear-g5b116754', + 'en', + 'http://en.example.com' + ) + ); + middlewareWithPathnames( + createMockRequest( + '/products/apparel/t-shirts', + 'en', + 'http://en.example.com' + ) + ); + middlewareWithPathnames( + createMockRequest( + '/categories/women/t-shirts', + 'en', + 'http://en.example.com' + ) + ); + + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalledTimes(6); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://en.example.com/en/about' + ); + expect(MockedNextResponse.rewrite.mock.calls[1][0].toString()).toBe( + 'http://en.example.com/en/users' + ); + expect(MockedNextResponse.rewrite.mock.calls[2][0].toString()).toBe( + 'http://en.example.com/en/users/1' + ); + expect(MockedNextResponse.rewrite.mock.calls[3][0].toString()).toBe( + 'http://en.example.com/en/news/happy-newyear-g5b116754' + ); + expect(MockedNextResponse.rewrite.mock.calls[4][0].toString()).toBe( + 'http://en.example.com/en/products/apparel/t-shirts' + ); + expect(MockedNextResponse.rewrite.mock.calls[5][0].toString()).toBe( + 'http://en.example.com/en/categories/women/t-shirts' + ); + }); + + it('serves requests for a non-default locale at the root', () => { + middlewareWithPathnames( + createMockRequest('/fr', 'fr', 'http://ca.example.com') + ); + expect(MockedNextResponse.rewrite).toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); // We rewrite just in case + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://ca.example.com/fr' + ); + }); + + it('serves requests for a non-default locale at nested paths', () => { + middlewareWithPathnames( + createMockRequest('/fr/a-propos', 'fr', 'http://ca.example.com') + ); + middlewareWithPathnames( + createMockRequest('/fr/utilisateurs', 'fr', 'http://ca.example.com') + ); + middlewareWithPathnames( + createMockRequest('/fr/utilisateurs/1', 'fr', 'http://ca.example.com') + ); + middlewareWithPathnames( + createMockRequest( + '/fr/nouvelles/happy-newyear-g5b116754', + 'fr', + 'http://ca.example.com' + ) + ); + middlewareWithPathnames( + createMockRequest( + '/fr/produits/vetements/t-shirts', + 'fr', + 'http://ca.example.com' + ) + ); + middlewareWithPathnames( + createMockRequest( + '/fr/categories/femmes/t-shirts', + 'fr', + 'http://ca.example.com' + ) + ); + + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalledTimes(6); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://ca.example.com/fr/about' + ); + expect(MockedNextResponse.rewrite.mock.calls[1][0].toString()).toBe( + 'http://ca.example.com/fr/users' + ); + expect(MockedNextResponse.rewrite.mock.calls[2][0].toString()).toBe( + 'http://ca.example.com/fr/users/1' + ); + expect(MockedNextResponse.rewrite.mock.calls[3][0].toString()).toBe( + 'http://ca.example.com/fr/news/happy-newyear-g5b116754' + ); + expect(MockedNextResponse.rewrite.mock.calls[4][0].toString()).toBe( + 'http://ca.example.com/fr/products/vetements/t-shirts' + ); + expect(MockedNextResponse.rewrite.mock.calls[5][0].toString()).toBe( + 'http://ca.example.com/fr/categories/femmes/t-shirts' + ); + }); + + it('redirects a request for a localized route that is not associated with the requested locale', () => { + middlewareWithPathnames( + createMockRequest('/a-propos', 'en', 'http://en.example.com') + ); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).toHaveBeenCalledTimes(1); + expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( + 'http://en.example.com/about' + ); + }); + + it('redirects when a pathname from the default locale ends up with a different locale that is the default locale on the domain', () => { + // Relevant to avoid duplicate content issues + middlewareWithPathnames( + createMockRequest('/about', 'fr', 'http://fr.example.com') + ); + middlewareWithPathnames( + createMockRequest('/users/2', 'fr', 'http://fr.example.com') + ); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).toHaveBeenCalledTimes(2); + expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( + 'http://fr.example.com/a-propos' + ); + expect(MockedNextResponse.redirect.mock.calls[1][0].toString()).toBe( + 'http://fr.example.com/utilisateurs/2' + ); + }); + + it('redirects when a pathname from the default locale ends up with a different locale that is a secondary locale on the domain', () => { + // Relevant to avoid duplicate content issues + middlewareWithPathnames( + createMockRequest('/about', 'fr', 'http://ca.example.com') + ); + middlewareWithPathnames( + createMockRequest('/users/2', 'fr', 'http://ca.example.com') + ); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).toHaveBeenCalledTimes(2); + expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( + 'http://ca.example.com/fr/a-propos' + ); + expect(MockedNextResponse.redirect.mock.calls[1][0].toString()).toBe( + 'http://ca.example.com/fr/utilisateurs/2' + ); + }); + + it('redirects a non-prefixed nested path to a localized alternative if another locale was detected', () => { + middlewareWithPathnames( + createMockRequest('/about', 'fr', 'http://ca.example.com') + ); + middlewareWithPathnames( + createMockRequest('/users/2', 'fr', 'http://ca.example.com') + ); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).toHaveBeenCalledTimes(2); + expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( + 'http://ca.example.com/fr/a-propos' + ); + expect(MockedNextResponse.redirect.mock.calls[1][0].toString()).toBe( + 'http://ca.example.com/fr/utilisateurs/2' + ); + }); + + it('sets alternate links', () => { + function getLinks(request: NextRequest) { + return middlewareWithPathnames(request) + .headers.get('link') + ?.split(', '); + } + + expect( + getLinks(createMockRequest('/', 'en', 'http://en.example.com')) + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="fr"', + '; rel="alternate"; hreflang="fr"' + ]); + expect( + getLinks(createMockRequest('/fr', 'fr', 'http://ca.example.com')) + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="fr"', + '; rel="alternate"; hreflang="fr"' + ]); + expect( + getLinks(createMockRequest('/about', 'en', 'http://en.example.com')) + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="fr"', + '; rel="alternate"; hreflang="fr"' + ]); + expect( + getLinks( + createMockRequest('/a-propos', 'fr', 'http://ca.example.com') + ) + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="fr"', + '; rel="alternate"; hreflang="fr"' + ]); + expect( + getLinks(createMockRequest('/users/1', 'en', 'http://en.example.com')) + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="fr"', + '; rel="alternate"; hreflang="fr"' + ]); + expect( + getLinks( + createMockRequest('/utilisateurs/1', 'fr', 'http://fr.example.com') + ) + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="fr"', + '; rel="alternate"; hreflang="fr"' + ]); + expect( + getLinks( + createMockRequest( + '/products/apparel/t-shirts', + 'en', + 'http://en.example.com' + ) + ) + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="fr"', + '; rel="alternate"; hreflang="fr"' + ]); + expect( + getLinks( + createMockRequest( + '/fr/produits/apparel/t-shirts', + 'fr', + 'http://fr.example.com' + ) + ) + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="fr"', + '; rel="alternate"; hreflang="fr"' + ]); + expect( + getLinks(createMockRequest('/unknown', 'en', 'http://en.example.com')) + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="fr"', + '; rel="alternate"; hreflang="fr"' + ]); + expect( + getLinks( + createMockRequest('/fr/unknown', 'fr', 'http://ca.example.com') + ) + ).toEqual([ + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="en"', + '; rel="alternate"; hreflang="fr"', + '; rel="alternate"; hreflang="fr"' + ]); + }); + }); }); describe("localePrefix: 'always'", () => { @@ -784,26 +1757,32 @@ describe('domain-based routing', () => { it('serves requests for the default locale', () => { middleware(createMockRequest('/en', 'en', 'http://ca.example.com')); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); - expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.next).toHaveBeenCalledTimes(1); - middleware(createMockRequest('/en/about', 'en', 'http://ca.example.com')); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.next).toHaveBeenCalledTimes(2); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalledTimes(2); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://ca.example.com/en' + ); + expect(MockedNextResponse.rewrite.mock.calls[1][0].toString()).toBe( + 'http://ca.example.com/en/about' + ); }); it('serves requests for non-default locales', () => { middleware(createMockRequest('/fr', 'fr', 'http://ca.example.com')); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); - expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.next).toHaveBeenCalled(); - middleware(createMockRequest('/fr/about', 'fr', 'http://ca.example.com')); - expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.next).toHaveBeenCalledTimes(2); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalledTimes(2); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://ca.example.com/fr' + ); + expect(MockedNextResponse.rewrite.mock.calls[1][0].toString()).toBe( + 'http://ca.example.com/fr/about' + ); }); }); }); diff --git a/packages/next-intl/test/middleware/utils.test.tsx b/packages/next-intl/test/middleware/utils.test.tsx new file mode 100644 index 000000000..969170938 --- /dev/null +++ b/packages/next-intl/test/middleware/utils.test.tsx @@ -0,0 +1,125 @@ +import {describe, expect, it} from 'vitest'; +import { + formatPathname, + getNormalizedPathname, + getRouteParams +} from '../../src/middleware/utils'; + +describe('getNormalizedPathname', () => { + it('should return the normalized pathname', () => { + expect(getNormalizedPathname('/en/about', ['en', 'de'])).toBe('/about'); + expect(getNormalizedPathname('/de/about', ['en', 'de'])).toBe('/about'); + expect(getNormalizedPathname('/about', ['en', 'de'])).toBe('/about'); + expect(getNormalizedPathname('/', ['en', 'de'])).toBe('/'); + expect(getNormalizedPathname('/es', ['en', 'de'])).toBe('/es'); + }); +}); + +describe('getRouteParams', () => { + it('returns undefined for non-matching paths', () => { + expect( + getRouteParams('/users/[userId]-[userName]', '/posts/42') + ).toBeUndefined(); + expect(getRouteParams('/users/[userId]', '/posts/42')).toBeUndefined(); + expect( + getRouteParams('/users/[userId]/posts/[postId]', '/users/23/comments/42') + ).toBeUndefined(); + }); + + it('returns an object with parameters for matching paths', () => { + expect( + getRouteParams('/users/[userId]-[userName]', '/users/23-jane') + ).toEqual({userId: '23', userName: 'jane'}); + expect(getRouteParams('/users/[userId]', '/users/23')).toEqual({ + userId: '23' + }); + expect( + getRouteParams('/users/[userId]/posts/[postId]', '/users/23/posts/42') + ).toEqual({userId: '23', postId: '42'}); + }); + + it('handles special characters in parameter values', () => { + expect(getRouteParams('/users/[userId]', '/users/23%20jane')).toEqual({ + userId: '23%20jane' + }); + expect(getRouteParams('/users/[userId]', '/users/23%2F42')).toEqual({ + userId: '23%2F42' + }); + }); + + it('handles arrays', () => { + expect( + getRouteParams( + '/categories/[...categories]', + '/categories/clothing/t-shirts' + ) + ).toEqual({ + '...categories': 'clothing/t-shirts' + }); + expect( + getRouteParams( + '/categories/[[...categories]]', + '/categories/clothing/t-shirts' + ) + ).toEqual({ + '...categories': 'clothing/t-shirts' + }); + }); +}); + +describe('formatPathname', () => { + it('returns the template if no params are provided', () => { + expect(formatPathname('/users')).toBe('/users'); + expect(formatPathname('/users/[userId]-[userName]')).toBe( + '/users/[userId]-[userName]' + ); + expect(formatPathname('/users/[userId]/posts/[postId]')).toBe( + '/users/[userId]/posts/[postId]' + ); + }); + + it('replaces parameter placeholders with values', () => { + expect( + formatPathname('/users/[userId]-[userName]', { + userId: '23', + userName: 'jane' + }) + ).toBe('/users/23-jane'); + expect(formatPathname('/users/[userId]', {userId: '23'})).toBe('/users/23'); + expect( + formatPathname('/users/[userId]/posts/[postId]', { + userId: '23', + postId: '42' + }) + ).toBe('/users/23/posts/42'); + }); + + it('ignores extra parameters', () => { + expect( + formatPathname('/users/[userId]-[userName]', { + userId: '23', + userName: 'jane', + extra: 'param' + }) + ).toBe('/users/23-jane'); + expect( + formatPathname('/users/[userId]', {userId: '23', extra: 'param'}) + ).toBe('/users/23'); + expect( + formatPathname('/users/[userId]/posts/[postId]', { + userId: '23', + postId: '42', + extra: 'param' + }) + ).toBe('/users/23/posts/42'); + }); + + it('does not encode special characters in parameter values', () => { + expect(formatPathname('/users/[userId]', {userId: '23%20jane'})).toBe( + '/users/23%20jane' + ); + expect(formatPathname('/users/[userId]', {userId: '23/42'})).toBe( + '/users/23/42' + ); + }); +}); diff --git a/packages/next-intl/test/link/Link.test.tsx b/packages/next-intl/test/navigation/BaseLink.test.tsx similarity index 77% rename from packages/next-intl/test/link/Link.test.tsx rename to packages/next-intl/test/navigation/BaseLink.test.tsx index d6330fd36..fbabff36c 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,24 +14,26 @@ 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' + '/test?foo=bar' ); }); 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,18 +79,25 @@ describe('unprefixed routing', () => { let ref; render( - { ref = node; }} href="/test" > Test - + ); expect(ref).toBeDefined(); }); + + it('sets an hreflang', () => { + render(Test); + expect( + screen.getByRole('link', {name: 'Test'}).getAttribute('hreflang') + ).toBe('en'); + }); }); describe('prefixed routing', () => { @@ -98,14 +107,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 +122,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 +133,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 +143,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 +151,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 +175,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 +184,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/navigation/createLocalizedPathnamesNavigation.test.tsx b/packages/next-intl/test/navigation/createLocalizedPathnamesNavigation.test.tsx new file mode 100644 index 000000000..5a5c90b8d --- /dev/null +++ b/packages/next-intl/test/navigation/createLocalizedPathnamesNavigation.test.tsx @@ -0,0 +1,525 @@ +import {render, screen} from '@testing-library/react'; +import { + usePathname as useNextPathname, + useParams, + useRouter as useNextRouter, + redirect as nextRedirect +} from 'next/navigation'; +import React, {ComponentProps} from 'react'; +import {it, describe, vi, beforeEach, expect, Mock} from 'vitest'; +import { + Pathnames, + createLocalizedPathnamesNavigation +} from '../../src/navigation'; + +vi.mock('next/navigation'); + +const locales = ['en', 'de'] as const; +const pathnames = { + '/': '/', + '/about': { + en: '/about', + de: '/ueber-uns' + }, + '/news/[articleSlug]-[articleId]': { + en: '/news/[articleSlug]-[articleId]', + de: '/neuigkeiten/[articleSlug]-[articleId]' + }, + '/categories/[...parts]': { + en: '/categories/[...parts]', + de: '/kategorien/[...parts]' + }, + '/catch-all/[[...parts]]': '/catch-all/[[...parts]]' +} satisfies Pathnames; + +const {Link, getPathname, redirect, usePathname, useRouter} = + createLocalizedPathnamesNavigation({ + locales, + pathnames + }); + +beforeEach(() => { + const router = { + push: vi.fn(), + replace: vi.fn(), + prefetch: vi.fn(), + back: vi.fn(), + forward: vi.fn(), + refresh: vi.fn() + }; + vi.mocked(useNextRouter).mockImplementation(() => router); + + // usePathname from Next.js returns the pathname the user sees + // (i.e. the external one that might be localized) + vi.mocked(useNextPathname).mockImplementation(() => '/'); + + vi.mocked(useParams).mockImplementation(() => ({locale: 'en'})); +}); + +describe('redirect', () => { + it('can redirect for the default locale', () => { + function Component({ + href + }: { + href: Parameters>[0]; + }) { + redirect(href); + return null; + } + + vi.mocked(useNextPathname).mockImplementation(() => '/'); + const {rerender} = render(); + expect(nextRedirect).toHaveBeenLastCalledWith('/en'); + + rerender(); + expect(nextRedirect).toHaveBeenLastCalledWith('/en/about'); + + rerender( + + ); + expect(nextRedirect).toHaveBeenLastCalledWith('/en/news/launch-party-3'); + }); + + it('can redirect for a non-default locale', () => { + vi.mocked(useParams).mockImplementation(() => ({locale: 'de'})); + function Component({ + href + }: { + href: Parameters>[0]; + }) { + redirect(href); + return null; + } + vi.mocked(useNextPathname).mockImplementation(() => '/'); + const {rerender} = render(); + expect(nextRedirect).toHaveBeenLastCalledWith('/de'); + + rerender(); + expect(nextRedirect).toHaveBeenLastCalledWith('/de/ueber-uns'); + + rerender( + + ); + expect(nextRedirect).toHaveBeenLastCalledWith( + '/de/neuigkeiten/launch-party-3' + ); + }); + + it('supports optional search params', () => { + function Component({ + href + }: { + href: Parameters>[0]; + }) { + redirect(href); + return null; + } + + vi.mocked(useNextPathname).mockImplementation(() => '/'); + render( + + ); + expect(nextRedirect).toHaveBeenLastCalledWith('/en?foo=bar&bar=1&bar=2'); + }); + + it('handles unknown route', () => { + function Component({ + href + }: { + href: Parameters>[0]; + }) { + redirect(href); + return null; + } + + vi.mocked(useNextPathname).mockImplementation(() => '/'); + // @ts-expect-error -- Unknown route + render(); + expect(nextRedirect).toHaveBeenLastCalledWith('/en/unknown'); + }); +}); + +describe('usePathname', () => { + it('returns the internal pathname for the default locale', () => { + function Component() { + const pathname = usePathname(); + return <>{pathname}; + } + vi.mocked(useNextPathname).mockImplementation(() => '/'); + const {rerender} = render(); + screen.getByText('/'); + + vi.mocked(useNextPathname).mockImplementation(() => '/about'); + rerender(); + screen.getByText('/about'); + + vi.mocked(useNextPathname).mockImplementation(() => '/news/launch-party-3'); + rerender(); + screen.getByText('/news/[articleSlug]-[articleId]'); + }); + + it('returns the internal pathname a non-default locale', () => { + vi.mocked(useParams).mockImplementation(() => ({locale: 'de'})); + + function Component() { + const pathname = usePathname(); + return <>{pathname}; + } + vi.mocked(useNextPathname).mockImplementation(() => '/de'); + const {rerender} = render(); + screen.getByText('/'); + + vi.mocked(useNextPathname).mockImplementation(() => '/de/ueber-uns'); + rerender(); + screen.getByText('/about'); + + vi.mocked(useNextPathname).mockImplementation( + () => '/de/neuigkeiten/launch-party-3' + ); + rerender(); + screen.getByText('/news/[articleSlug]-[articleId]'); + }); + + it('handles unknown route', () => { + function Component() { + const pathname = usePathname(); + return <>{pathname}; + } + vi.mocked(useNextPathname).mockImplementation(() => '/en/unknown'); + const {rerender} = render(); + screen.getByText('/unknown'); + + vi.mocked(useNextPathname).mockImplementation(() => '/de/unknown'); + rerender(); + screen.getByText('/de/unknown'); + }); +}); + +describe('Link', () => { + it('renders an href', () => { + render(About); + expect(screen.getByRole('link', {name: 'About'}).getAttribute('href')).toBe( + '/about' + ); + }); + + it('adds a prefix when linking to a non-default locale', () => { + render( + + Über uns + + ); + expect( + screen.getByRole('link', {name: 'Über uns'}).getAttribute('href') + ).toBe('/de/ueber-uns'); + }); + + it('handles params', () => { + render( + + About + + ); + expect(screen.getByRole('link', {name: 'About'}).getAttribute('href')).toBe( + '/de/neuigkeiten/launch-party-3' + ); + }); + + it('handles catch-all segments', () => { + render( + + Test + + ); + expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe( + '/categories/clothing/t-shirts' + ); + }); + + it('handles optional catch-all segments', () => { + render( + + Test + + ); + expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe( + '/catch-all/one/two' + ); + }); + + it('supports optional search params', () => { + render( + + Test + + ); + expect(screen.getByRole('link', {name: 'Test'}).getAttribute('href')).toBe( + '/about?foo=bar&bar=1&bar=2' + ); + }); + + it('handles unknown routes', () => { + // @ts-expect-error -- Unknown route + const {rerender} = render(Unknown); + expect( + screen.getByRole('link', {name: 'Unknown'}).getAttribute('href') + ).toBe('/unknown'); + + rerender( + // @ts-expect-error -- Unknown route + + Unknown + + ); + expect( + screen.getByRole('link', {name: 'Unknown'}).getAttribute('href') + ).toBe('/de/unknown'); + }); +}); + +describe('getPathname', () => { + it('resolves to the correct path', () => { + expect( + getPathname({ + locale: 'en', + href: { + pathname: '/categories/[...parts]', + params: {parts: ['clothing', 't-shirts']}, + query: {sort: 'price'} + } + }) + ).toBe('/categories/clothing/t-shirts?sort=price'); + }); +}); + +describe('useRouter', () => { + describe('push', () => { + it('resolves to the correct path when passing another locale', () => { + function Component() { + const router = useRouter(); + router.push('/about', {locale: 'de'}); + return null; + } + render(); + const push = useNextRouter().push as Mock; + expect(push).toHaveBeenCalledTimes(1); + expect(push).toHaveBeenCalledWith('/de/ueber-uns'); + }); + + it('supports optional search params', () => { + function Component() { + const router = useRouter(); + router.push( + { + pathname: '/about', + query: { + foo: 'bar', + bar: [1, 2] + } + }, + {locale: 'de'} + ); + return null; + } + render(); + const push = useNextRouter().push as Mock; + expect(push).toHaveBeenCalledTimes(1); + expect(push).toHaveBeenCalledWith('/de/ueber-uns?foo=bar&bar=1&bar=2'); + }); + }); + + it('handles unknown routes', () => { + function Component() { + const router = useRouter(); + // @ts-expect-error -- Unknown route + router.push('/unknown'); + return null; + } + render(); + const push = useNextRouter().push as Mock; + expect(push).toHaveBeenCalledTimes(1); + expect(push).toHaveBeenCalledWith('/unknown'); + }); +}); + +/** + * Type tests + */ + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function TypeTests() { + const router = useRouter(); + + // @ts-expect-error -- Unknown route + router.push('/unknown'); + + // Valid + router.push('/about'); + router.push('/about', {locale: 'de'}); + router.push({pathname: '/about'}); + router.push('/catch-all/[[...parts]]'); + + // @ts-expect-error -- Requires params + router.push({pathname: '/news/[articleSlug]-[articleId]'}); + + router.push({ + pathname: '/news/[articleSlug]-[articleId]', + // @ts-expect-error -- Missing param + params: { + articleId: 3 + } + }); + + // Valid + router.push({ + pathname: '/news/[articleSlug]-[articleId]', + params: { + articleId: 3, + articleSlug: 'launch-party' + } + }); + + // @ts-expect-error -- Doesn't accept params + router.push({pathname: '/about', params: {foo: 'bar'}}); + + // @ts-expect-error -- Unknown locale + + Über uns + ; + + // @ts-expect-error -- Unknown route + About; + + // @ts-expect-error -- Requires params + About; + // @ts-expect-error -- Requires params + About; + + // @ts-expect-error -- Params for different route + About; + + // @ts-expect-error -- Doesn't accept params + About; + + // @ts-expect-error -- Missing params + Über uns; + + // Valid + Über uns; + Über uns; + + Über uns + ; + Optional catch-all; + + // Link composition + function WrappedLink( + props: ComponentProps> + ) { + return ; + } + About; + + News + ; + + // @ts-expect-error -- Requires params + News; + + // Valid + redirect({pathname: '/about'}); + redirect('/catch-all/[[...parts]]'); + redirect({ + pathname: '/catch-all/[[...parts]]', + params: {parts: ['one', 'two']} + }); + + // @ts-expect-error -- Unknown route + redirect('/unknown'); + // @ts-expect-error -- Localized alternative + redirect('/ueber-uns'); + // @ts-expect-error -- Requires params + redirect('/news/[articleSlug]-[articleId]'); + redirect({ + pathname: '/news/[articleSlug]-[articleId]', + // @ts-expect-error -- Missing param + params: { + articleId: 3 + } + }); + + // Allow unknown routes + const { + Link: LinkWithUnknown, + redirect: redirectWithUnknown, + usePathname: usePathnameWithUnkown, + useRouter: useRouterWithUnknown + } = createLocalizedPathnamesNavigation({ + locales, + // eslint-disable-next-line @typescript-eslint/ban-types + pathnames: pathnames as typeof pathnames & Record + }); + Unknown; + redirectWithUnknown('/unknown'); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const pathnameWithUnknown: ReturnType = + '/unknown'; + useRouterWithUnknown().push('/unknown'); +} diff --git a/packages/next-intl/test/navigation/createSharedPathnamesNavigation.test.tsx b/packages/next-intl/test/navigation/createSharedPathnamesNavigation.test.tsx new file mode 100644 index 000000000..4749d29d4 --- /dev/null +++ b/packages/next-intl/test/navigation/createSharedPathnamesNavigation.test.tsx @@ -0,0 +1,114 @@ +import {render, screen} from '@testing-library/react'; +import { + usePathname, + useParams, + useRouter as useNextRouter +} from 'next/navigation'; +import React from 'react'; +import {it, describe, vi, beforeEach, expect, Mock} from 'vitest'; +import createSharedPathnamesNavigation from '../../src/navigation/createSharedPathnamesNavigation'; + +vi.mock('next/navigation'); + +const {Link, useRouter} = createSharedPathnamesNavigation({ + locales: ['en', 'de'] as const +}); + +beforeEach(() => { + const router = { + push: vi.fn(), + replace: vi.fn(), + prefetch: vi.fn(), + back: vi.fn(), + forward: vi.fn(), + refresh: vi.fn() + }; + vi.mocked(useNextRouter).mockImplementation(() => router); + vi.mocked(usePathname).mockImplementation(() => '/'); + vi.mocked(useParams).mockImplementation(() => ({locale: 'en'})); +}); + +describe('Link', () => { + it('renders an href', () => { + render(About); + expect(screen.getByRole('link', {name: 'About'}).getAttribute('href')).toBe( + '/about' + ); + }); + + it('renders an object href', () => { + render(About); + expect(screen.getByRole('link', {name: 'About'}).getAttribute('href')).toBe( + '/about?foo=bar' + ); + }); + + it('adds a prefix when linking to a non-default locale', () => { + render( + + Über uns + + ); + expect( + screen.getByRole('link', {name: 'Über uns'}).getAttribute('href') + ).toBe('/de/about'); + }); + + it('handles params', () => { + render( + + About + + ); + expect(screen.getByRole('link', {name: 'About'}).getAttribute('href')).toBe( + '/de/news/launch-party-3' + ); + }); +}); + +describe('useRouter', () => { + describe('push', () => { + it('resolves to the correct path when passing another locale', () => { + function Component() { + const router = useRouter(); + router.push('/about', {locale: 'de'}); + return null; + } + render(); + const push = useNextRouter().push as Mock; + expect(push).toHaveBeenCalledTimes(1); + expect(push).toHaveBeenCalledWith('/de/about'); + }); + }); +}); + +/** + * Type tests + */ + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function TypeTests() { + const router = useRouter(); + + // @ts-expect-error -- Only supports string paths + router.push({pathname: '/about'}); + + // Valid + router.push('/about'); + router.push('/about', {locale: 'de'}); + router.push('/unknown'); // No error since routes are unknown + + // @ts-expect-error -- No params supported + + User + ; + + // @ts-expect-error -- Unknown locale + + User + ; + + // Valid + Über uns; + About; // No error since routes are unknown +} 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/test/navigation/utils.test.tsx b/packages/next-intl/test/navigation/utils.test.tsx new file mode 100644 index 000000000..bdf2c0eb7 --- /dev/null +++ b/packages/next-intl/test/navigation/utils.test.tsx @@ -0,0 +1,44 @@ +import {describe, expect, it} from 'vitest'; +import { + compileLocalizedPathname, + serializeSearchParams +} from '../../src/navigation/utils'; + +describe('serializeSearchParams', () => { + it('handles strings', () => { + expect(serializeSearchParams({v: 'test'})).toEqual('?v=test'); + }); + + it('handles numbers', () => { + expect(serializeSearchParams({v: 2})).toEqual('?v=2'); + }); + + it('handles booleans', () => { + expect(serializeSearchParams({v: true})).toEqual('?v=true'); + }); + + it('handles arrays', () => { + expect(serializeSearchParams({v: ['a', 'b']})).toEqual('?v=a&v=b'); + }); +}); + +describe('compileLocalizedPathname', () => { + it('throws when params were not resolved', () => { + const locales: ReadonlyArray = ['en']; + expect(() => + // @ts-expect-error -- Purposefully miss a param + compileLocalizedPathname({ + locale: 'en', + pathname: '/test/[one]/[two]', + pathnames: '/test/[one]/[two]', + params: {one: '1'} + }) + ).toThrow( + [ + 'Insufficient params provided for localized pathname.', + 'Template: /test/[one]/[two]', + 'Params: {"one":"1"}' + ].join('\n') + ); + }); +}); diff --git a/packages/next-intl/test/react-client/useFormatter.test.tsx b/packages/next-intl/test/react-client/useFormatter.test.tsx new file mode 100644 index 000000000..265701909 --- /dev/null +++ b/packages/next-intl/test/react-client/useFormatter.test.tsx @@ -0,0 +1,24 @@ +import {render, screen} from '@testing-library/react'; +import React from 'react'; +import {it, expect} from 'vitest'; +import {useFormatter, NextIntlClientProvider} from '../../src'; + +function Component() { + const format = useFormatter(); + return <>{format.number(1)}; +} + +it('provides a helpful error message when no provider is found', () => { + expect(() => render()).toThrow( + /Failed to call `useFormatter` because the context from `NextIntlClientProvider` was not found\./ + ); +}); + +it('works with a provider', () => { + render( + + + + ); + screen.getByText('1'); +}); diff --git a/packages/next-intl/test/react-client/useLocale.test.tsx b/packages/next-intl/test/react-client/useLocale.test.tsx new file mode 100644 index 000000000..a095f5cf4 --- /dev/null +++ b/packages/next-intl/test/react-client/useLocale.test.tsx @@ -0,0 +1,17 @@ +import {render, screen} from '@testing-library/react'; +import React from 'react'; +import {it, vi} from 'vitest'; +import {useLocale} from '../../src'; + +vi.mock('next/navigation', () => ({ + useParams: () => ({locale: 'en'}) +})); + +it('returns a locale from `useParams` without a provider', () => { + function Component() { + return <>{useLocale()}; + } + + render(); + screen.getByText('en'); +}); diff --git a/packages/next-intl/test/react-client/useNow.test.tsx b/packages/next-intl/test/react-client/useNow.test.tsx new file mode 100644 index 000000000..0546c6976 --- /dev/null +++ b/packages/next-intl/test/react-client/useNow.test.tsx @@ -0,0 +1,29 @@ +import {render, screen} from '@testing-library/react'; +import React from 'react'; +import {it} from 'vitest'; +import {useNow, NextIntlClientProvider} from '../../src'; + +function Component() { + const now = useNow(); + return <>{now.toISOString()}; +} + +it('works without a provider', () => { + render( + + + + ); +}); + +it('works with a provider', () => { + render( + + + + ); + screen.getByText('2021-01-01T00:00:00.000Z'); +}); diff --git a/packages/next-intl/test/react-client/useTimeZone.test.tsx b/packages/next-intl/test/react-client/useTimeZone.test.tsx new file mode 100644 index 000000000..3057277bf --- /dev/null +++ b/packages/next-intl/test/react-client/useTimeZone.test.tsx @@ -0,0 +1,26 @@ +import {render, screen} from '@testing-library/react'; +import React from 'react'; +import {it} from 'vitest'; +import {useTimeZone, NextIntlClientProvider} from '../../src'; + +function Component() { + const timeZone = useTimeZone(); + return <>{timeZone}; +} + +it('works without a provider', () => { + render( + + + + ); +}); + +it('works with a provider', () => { + render( + + + + ); + screen.getByText('America/New_York'); +}); diff --git a/packages/next-intl/test/react-client/useTranslations.test.tsx b/packages/next-intl/test/react-client/useTranslations.test.tsx new file mode 100644 index 000000000..edab637e7 --- /dev/null +++ b/packages/next-intl/test/react-client/useTranslations.test.tsx @@ -0,0 +1,35 @@ +import {render, screen} from '@testing-library/react'; +import React from 'react'; +import {it, expect, vi} from 'vitest'; +import {useTranslations, NextIntlClientProvider} from '../../src'; + +function Component() { + const t = useTranslations('Component'); + return <>{t('test')}; +} + +it('provides a helpful error message when no provider is found', () => { + expect(() => render()).toThrow( + /Failed to call `useTranslations` because the context from `NextIntlClientProvider` was not found\./ + ); +}); + +it('works with a provider', () => { + render( + + + + ); + screen.getByText('Hello'); +}); + +it('uses error handling for missing messages', () => { + const onError = vi.fn(); + render( + + + + ); + screen.getByText('Component.test'); + expect(onError).toHaveBeenCalled(); +}); diff --git a/packages/next-intl/test/server/index.test.tsx b/packages/next-intl/test/server/index.test.tsx new file mode 100644 index 000000000..f464f3306 --- /dev/null +++ b/packages/next-intl/test/server/index.test.tsx @@ -0,0 +1,129 @@ +// @vitest-environment edge-runtime + +import {it, vi, expect, describe} from 'vitest'; +import { + getTranslations, + getMessages, + getFormatter, + getNow, + getTimeZone +} from '../../src/server'; +import {HEADER_LOCALE_NAME} from '../../src/shared/constants'; + +vi.mock('next-intl/config', () => ({ + default: async () => + ((await vi.importActual('../../src/server')) as any).getRequestConfig({ + locale: 'en', + now: new Date('2020-01-01T00:00:00.000Z'), + timeZone: 'Europe/London', + messages: { + About: { + basic: 'Hello', + interpolation: 'Hello {name}', + rich: '{name}' + } + } + }) +})); + +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 { + ...React, + cache(fn: (...args: Array) => unknown) { + return (...args: Array) => fn(...args); + } + }; +}); + +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 getTranslations({locale: 'en', namespace: 'About'}); + expect(t('interpolation', {name: 'Jane'})).toBe('Hello Jane'); + }); + + it('renders rich text to a string', async () => { + const t = await getTranslations({locale: 'en', namespace: 'About'}); + expect( + t.rich('rich', { + name: 'Example', + link: (chunks) => `${chunks}` + }) + ).toBe('Example'); + }); + + it('renders raw text to a string', async () => { + 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({locale: 'en'}); + expect(format.dateTime(new Date('2020-01-01T00:00:00.000Z'))).toBe( + '1/1/2020' + ); + }); +}); + +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({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({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({locale: 'en'})).toBe('Europe/London'); + }); +}); diff --git a/packages/next-intl/test/shared/NextIntlClientProvider.test.tsx b/packages/next-intl/test/shared/NextIntlClientProvider.test.tsx index e14321472..1da1f675d 100644 --- a/packages/next-intl/test/shared/NextIntlClientProvider.test.tsx +++ b/packages/next-intl/test/shared/NextIntlClientProvider.test.tsx @@ -1,12 +1,8 @@ import {render, screen} from '@testing-library/react'; import React from 'react'; -import {it, vi} from 'vitest'; +import {it} from 'vitest'; import {useTranslations, NextIntlClientProvider} from '../../src'; -vi.mock('next/router', () => ({ - useRouter: () => ({locale: 'en'}) -})); - it('can use messages from the provider', () => { function Component() { const t = useTranslations(); @@ -14,7 +10,7 @@ it('can use messages from the provider', () => { } render( - + ); diff --git a/packages/next-intl/test/shared/utils.test.tsx b/packages/next-intl/test/shared/utils.test.tsx index be777c42e..7d84bd415 100644 --- a/packages/next-intl/test/shared/utils.test.tsx +++ b/packages/next-intl/test/shared/utils.test.tsx @@ -1,5 +1,24 @@ import {it, describe, expect} from 'vitest'; -import {hasPathnamePrefixed, unlocalizePathname} from '../../src/shared/utils'; +import { + hasPathnamePrefixed, + unlocalizePathname, + matchesPathname, + localizePathname +} from '../../src/shared/utils'; + +describe('localizePathname', () => { + it("doesn't add trailing slashes for the root", () => { + expect(localizePathname('en', '/')).toEqual('/en'); + }); + + it("doesn't add trailing slashes for search params", () => { + expect(localizePathname('en', '/?foo=bar')).toEqual('/en?foo=bar'); + }); + + it('localizes nested paths', () => { + expect(localizePathname('en', '/nested')).toEqual('/en/nested'); + }); +}); describe('hasPathnamePrefixed', () => { it('detects prefixed pathnames', () => { @@ -31,3 +50,62 @@ describe('unlocalizePathname', () => { expect(unlocalizePathname('/en-UK/nested', 'en-UK')).toEqual('/nested'); }); }); + +describe('matchesPathname', () => { + it('returns true for matching paths', () => { + expect( + matchesPathname('/users/[userId]-[userName]', '/users/23-jane') + ).toBe(true); + expect( + matchesPathname( + '/users/[userId]-[userName]-hello', + '/users/23-jane-smith-hello' + ) + ).toBe(true); + expect(matchesPathname('/users/[userId]', '/users/23')).toBe(true); + expect( + matchesPathname('/users/[userId]/posts/[postId]', '/users/23/posts/42') + ).toBe(true); + expect( + matchesPathname('/products/[...slug]', '/products/clothing/t-shirts') + ).toBe(true); + expect(matchesPathname('/[[...slug]]', '/products/clothing/t-shirts')).toBe( + true + ); + }); + + it('returns false for non-matching paths', () => { + expect(matchesPathname('/users/[userId]-[userName]', '/users/23')).toBe( + false + ); + expect(matchesPathname('/users/[userId]', '/users/23/posts')).toBe(false); + expect( + matchesPathname('/users/[userId]/posts/[postId]', '/users/23/posts') + ).toBe(false); + }); + + it('returns false for paths with missing parameters', () => { + expect(matchesPathname('/users/[userId]-[userName]', '/users/')).toBe( + false + ); + expect(matchesPathname('/users/[userId]', '/users/')).toBe(false); + expect( + matchesPathname('/users/[userId]/posts/[postId]', '/users/23/posts/') + ).toBe(false); + }); + + it('returns false for paths with extra segments', () => { + expect( + matchesPathname('/users/[userId]-[userName]', '/users/23-jane/posts') + ).toBe(false); + expect(matchesPathname('/users/[userId]', '/users/23/posts/42')).toBe( + false + ); + expect( + matchesPathname( + '/users/[userId]/posts/[postId]', + '/users/23/posts/42/comments' + ) + ).toBe(false); + }); +}); diff --git a/packages/next-intl/tsconfig.json b/packages/next-intl/tsconfig.json index 3e96fb7bf..769cee18a 100644 --- a/packages/next-intl/tsconfig.json +++ b/packages/next-intl/tsconfig.json @@ -1,23 +1,18 @@ { + "extends": "eslint-config-molindo/tsconfig.json", "include": ["src", "test", "types", "next-env.d.ts"], "compilerOptions": { "module": "esnext", "lib": ["dom", "esnext"], - "target": "ES2019", + "target": "ES2020", "importHelpers": false, "declaration": true, "sourceMap": true, "rootDir": ".", - "strict": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUnusedLocals": true, - "noUnusedParameters": true, "moduleResolution": "node", "jsx": "react", "esModuleInterop": true, "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, "noEmit": false, "outDir": "dist" } diff --git a/packages/use-intl/README.md b/packages/use-intl/README.md index 0029d488a..d40163eac 100644 --- a/packages/use-intl/README.md +++ b/packages/use-intl/README.md @@ -35,7 +35,7 @@ export default function UserProfile({user}) { } ``` -```json +```js // en.json { "UserProfile": { diff --git a/packages/use-intl/_IntlProvider.d.ts b/packages/use-intl/_IntlProvider.d.ts new file mode 100644 index 000000000..638757cd7 --- /dev/null +++ b/packages/use-intl/_IntlProvider.d.ts @@ -0,0 +1 @@ +export * from './dist/types/src/_IntlProvider'; diff --git a/packages/use-intl/_useLocale.d.ts b/packages/use-intl/_useLocale.d.ts new file mode 100644 index 000000000..6eefcd371 --- /dev/null +++ b/packages/use-intl/_useLocale.d.ts @@ -0,0 +1 @@ +export * from './dist/types/src/_useLocale'; diff --git a/packages/use-intl/core.d.ts b/packages/use-intl/core.d.ts new file mode 100644 index 000000000..5df015235 --- /dev/null +++ b/packages/use-intl/core.d.ts @@ -0,0 +1 @@ +export * from './dist/types/src/core'; diff --git a/packages/use-intl/dts.config.js b/packages/use-intl/dts.config.js index 82f1fcb7b..0d711aa6f 100644 --- a/packages/use-intl/dts.config.js +++ b/packages/use-intl/dts.config.js @@ -1,7 +1,7 @@ /* global module */ /** - * @type {import('dts-cli').DtsConfig} + * @type {import('dts-cli').DtsOptions} */ module.exports = { rollup(config) { diff --git a/packages/use-intl/package.json b/packages/use-intl/package.json index 2c4dde887..1449896a8 100644 --- a/packages/use-intl/package.json +++ b/packages/use-intl/package.json @@ -11,20 +11,43 @@ "url": "https://github.com/amannn/next-intl/tree/main/packages/use-intl" }, "scripts": { - "build": "pnpm build:default && pnpm build:rsc", - "build:default": "rm -rf dist && dts build", - "build:rsc": "tsc && rm -rf dist/test", + "build": "rm -rf dist && rollup -c", "test": "TZ=Europe/Berlin vitest", "lint": "eslint src test && tsc --noEmit", "prepublishOnly": "CI=true turbo test && turbo lint && turbo build", "size": "size-limit" }, - "main": "dist/index.js", - "module": "dist/use-intl.esm.js", - "typings": "dist/index.d.ts", + "main": "./dist/index.js", + "module": "dist/esm/index.js", + "typings": "./dist/types/src/index.d.ts", + "exports": { + ".": { + "types": "./dist/types/src/index.d.ts", + "default": "./dist/index.js" + }, + "./core": { + "types": "./core.d.ts", + "default": "./dist/core.js" + }, + "./react": { + "types": "./react.d.ts", + "default": "./dist/react.js" + }, + "./_useLocale": { + "types": "./_useLocale.d.ts", + "default": "./dist/_useLocale.js" + }, + "./_IntlProvider": { + "types": "./_IntlProvider.d.ts", + "default": "./dist/_IntlProvider.js" + } + }, "files": [ "dist", - "src" + "core.d.ts", + "react.d.ts", + "_useLocale.d.ts", + "_IntlProvider.d.ts" ], "keywords": [ "react", @@ -48,28 +71,22 @@ "@size-limit/preset-big-lib": "^8.2.6", "@testing-library/react": "^13.0.0", "@types/node": "^17.0.23", - "@types/react": "^18.2.5", + "@types/react": "^18.2.29", + "@types/react-dom": "^18.2.5", "date-fns": "^2.16.1", - "dts-cli": "^1.4.0", "eslint": "^8.46.0", "eslint-config-molindo": "^7.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "rollup": "^3.28.1", "size-limit": "^8.2.6", - "typescript": "^5.0.0", + "typescript": "^5.2.2", "vitest": "^0.32.2" }, "size-limit": [ { - "path": "dist/use-intl.cjs.production.min.js", - "limit": "10.3 kB" - }, - { - "path": "dist/use-intl.cjs.development.js", - "limit": "10.3 kB" + "path": "dist/production/index.js", + "limit": "12.355 kB" } - ], - "engines": { - "node": ">=10" - } + ] } diff --git a/packages/use-intl/react.d.ts b/packages/use-intl/react.d.ts new file mode 100644 index 000000000..db798174f --- /dev/null +++ b/packages/use-intl/react.d.ts @@ -0,0 +1 @@ +export * from './dist/types/src/react'; diff --git a/packages/use-intl/rollup.config.js b/packages/use-intl/rollup.config.js new file mode 100644 index 000000000..032c94884 --- /dev/null +++ b/packages/use-intl/rollup.config.js @@ -0,0 +1,16 @@ +/* eslint-env node */ +const getBuildConfig = require('../../scripts/getBuildConfig'); + +const input = { + index: 'src/index.tsx', + core: 'src/core.tsx', + react: 'src/react.tsx', + _useLocale: 'src/_useLocale.tsx', + _IntlProvider: 'src/_IntlProvider.tsx' +}; + +module.exports = [ + getBuildConfig({input, env: 'development'}), + getBuildConfig({input, env: 'esm'}), + getBuildConfig({input, env: 'production'}) +]; diff --git a/packages/use-intl/src/_IntlProvider.tsx b/packages/use-intl/src/_IntlProvider.tsx new file mode 100644 index 000000000..c02594cc2 --- /dev/null +++ b/packages/use-intl/src/_IntlProvider.tsx @@ -0,0 +1 @@ +export {default as IntlProvider} from './react/IntlProvider'; diff --git a/packages/use-intl/src/_useLocale.tsx b/packages/use-intl/src/_useLocale.tsx new file mode 100644 index 000000000..162f5d506 --- /dev/null +++ b/packages/use-intl/src/_useLocale.tsx @@ -0,0 +1 @@ +export {default as useLocale} from './react/useLocale'; diff --git a/packages/use-intl/src/core.tsx b/packages/use-intl/src/core.tsx new file mode 100644 index 000000000..65c514e92 --- /dev/null +++ b/packages/use-intl/src/core.tsx @@ -0,0 +1 @@ +export * from './core/index'; 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/TranslationValues.tsx b/packages/use-intl/src/core/TranslationValues.tsx index 106f80182..3d4106f67 100644 --- a/packages/use-intl/src/core/TranslationValues.tsx +++ b/packages/use-intl/src/core/TranslationValues.tsx @@ -19,4 +19,9 @@ export type RichTranslationValues = Record< TranslationValue | ((chunks: ReactNode) => ReactNode) >; +export type MarkupTranslationValues = Record< + string, + TranslationValue | ((chunks: string) => string) +>; + export default TranslationValues; diff --git a/packages/use-intl/src/core/createBaseTranslator.tsx b/packages/use-intl/src/core/createBaseTranslator.tsx index 222644a37..c8d844217 100644 --- a/packages/use-intl/src/core/createBaseTranslator.tsx +++ b/packages/use-intl/src/core/createBaseTranslator.tsx @@ -12,7 +12,10 @@ import Formats from './Formats'; import {InitializedIntlConfig} from './IntlConfig'; import IntlError, {IntlErrorCode} from './IntlError'; import MessageFormatCache from './MessageFormatCache'; -import TranslationValues, {RichTranslationValues} from './TranslationValues'; +import TranslationValues, { + MarkupTranslationValues, + RichTranslationValues +} from './TranslationValues'; import convertFormatsToIntlMessageFormat from './convertFormatsToIntlMessageFormat'; import {defaultGetMessageFallback, defaultOnError} from './defaults'; import MessageKeys from './utils/MessageKeys'; @@ -81,12 +84,12 @@ function prepareTranslationValues(values: RichTranslationValues) { return transformedValues; } -export function getMessagesOrError({ +function getMessagesOrError({ messages, namespace, onError = defaultOnError }: { - messages: Messages; + messages?: Messages; namespace?: string; onError?(error: IntlError): void; }) { @@ -149,6 +152,22 @@ function getPlainMessage(candidate: string, values?: unknown) { export default function createBaseTranslator< Messages extends AbstractIntlMessages, NestedKey extends NestedKeyOf +>(config: Omit, 'messagesOrError'>) { + const messagesOrError = getMessagesOrError({ + messages: config.messages, + namespace: config.namespace, + onError: config.onError + }) as Messages | IntlError; + + return createBaseTranslatorImpl({ + ...config, + messagesOrError + }); +} + +function createBaseTranslatorImpl< + Messages extends AbstractIntlMessages, + NestedKey extends NestedKeyOf >({ defaultTranslationValues, formats: globalFormats, @@ -258,7 +277,7 @@ export default function createBaseTranslator< try { const formattedMessage = messageFormat.format( - // @ts-ignore `intl-messageformat` expects a different format + // @ts-expect-error `intl-messageformat` expects a different format // for rich text elements since a recent minor update. This // needs to be evaluated in detail, possibly also in regards // to be able to format to parts. @@ -323,6 +342,40 @@ export default function createBaseTranslator< translateFn.rich = translateBaseFn; + // Augment `translateBaseFn` to return plain strings + translateFn.markup = ( + key: Parameters[0], + /** Key value pairs for values to interpolate into the message. */ + values: MarkupTranslationValues, + formats?: Parameters[2] + ): string => { + const result = translateBaseFn( + key, + // @ts-expect-error -- `MarkupTranslationValues` is practically a sub type + // of `RichTranslationValues` but TypeScript isn't smart enough here. + values, + formats + ); + + // When only string chunks are provided to the parser, only + // strings should be returned here. Note that we need a runtime + // check for this since rich text values could be accidentally + // inherited from `defaultTranslationValues`. + if (typeof result !== 'string') { + const error = new IntlError( + IntlErrorCode.FORMATTING_ERROR, + process.env.NODE_ENV !== 'production' + ? "`t.markup` only accepts functions for formatting that receive and return strings.\n\nE.g. t.markup('markup', {b: (chunks) => `${chunks}`})" + : undefined + ); + + onError(error); + return getMessageFallback({error, key, namespace}); + } + + return result; + }; + translateFn.raw = ( /** Use a dot to indicate a level of nesting (e.g. `namespace.nestedLabel`). */ key: string 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/createTranslator.tsx b/packages/use-intl/src/core/createTranslator.tsx index e788c638c..27d4620a6 100644 --- a/packages/use-intl/src/core/createTranslator.tsx +++ b/packages/use-intl/src/core/createTranslator.tsx @@ -1,9 +1,12 @@ +import {ReactElement, ReactNodeArray} from 'react'; import Formats from './Formats'; import IntlConfig from './IntlConfig'; -import TranslationValues from './TranslationValues'; -import createTranslatorImpl, { - CoreRichTranslationValues -} from './createTranslatorImpl'; +import MessageFormatCache from './MessageFormatCache'; +import TranslationValues, { + MarkupTranslationValues, + RichTranslationValues +} from './TranslationValues'; +import createTranslatorImpl from './createTranslatorImpl'; import {defaultGetMessageFallback, defaultOnError} from './defaults'; import MessageKeys from './utils/MessageKeys'; import NamespaceKeys from './utils/NamespaceKeys'; @@ -30,8 +33,10 @@ export default function createTranslator< onError = defaultOnError, ...rest }: Omit, 'defaultTranslationValues' | 'messages'> & { - messages: NonNullable['messages']>; + messages: IntlConfig['messages']; namespace?: NestedKey; + /** @private */ + messageFormatCache?: MessageFormatCache; }): // Explicitly defining the return type is necessary as TypeScript would get it wrong { // Default invocation @@ -70,7 +75,27 @@ export default function createTranslator< > >( key: TargetKey, - values?: CoreRichTranslationValues, + 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; @@ -103,6 +128,7 @@ export default function createTranslator< ...rest, onError, getMessageFallback, + // @ts-expect-error `messages` is allowed to be `undefined` here and will be handled internally messages: {'!': messages}, namespace: namespace ? `!.${namespace}` : '!' }, diff --git a/packages/use-intl/src/core/createTranslatorImpl.tsx b/packages/use-intl/src/core/createTranslatorImpl.tsx index 999f3141d..9da0fe3b3 100644 --- a/packages/use-intl/src/core/createTranslatorImpl.tsx +++ b/packages/use-intl/src/core/createTranslatorImpl.tsx @@ -1,22 +1,17 @@ import AbstractIntlMessages from './AbstractIntlMessages'; import {InitializedIntlConfig} from './IntlConfig'; -import IntlError, {IntlErrorCode} from './IntlError'; -import {RichTranslationValues, TranslationValue} from './TranslationValues'; -import createBaseTranslator, {getMessagesOrError} from './createBaseTranslator'; +import MessageFormatCache from './MessageFormatCache'; +import createBaseTranslator from './createBaseTranslator'; import resolveNamespace from './resolveNamespace'; import NestedKeyOf from './utils/NestedKeyOf'; -export type CoreRichTranslationValues = Record< - string, - TranslationValue | ((chunks: string) => string) ->; - export type CreateTranslatorImplProps = Omit< InitializedIntlConfig, 'messages' > & { namespace: string; messages: Messages; + messageFormatCache?: MessageFormatCache; }; export default function createTranslatorImpl< @@ -37,51 +32,11 @@ export default function createTranslatorImpl< messages = messages[namespacePrefix] as Messages; namespace = resolveNamespace(namespace, namespacePrefix) as NestedKey; - const translator = createBaseTranslator({ + return createBaseTranslator({ ...rest, onError, getMessageFallback, - messagesOrError: getMessagesOrError({ - messages, - namespace, - onError - }) as Messages | IntlError + messages, + namespace }); - - const originalRich = translator.rich; - - function base(...args: Parameters) { - return translator(...args); - } - - // Augment `t.rich` to return plain strings - base.rich = ( - key: Parameters[0], - /** Key value pairs for values to interpolate into the message. */ - values: CoreRichTranslationValues, - formats?: Parameters[2] - ): string => { - // `chunks` is returned as a string when no React element - // is used, therefore it's safe to cast this type. - const result = originalRich(key, values as RichTranslationValues, formats); - - // When only string chunks are provided to the parser, only strings should be returned here. - if (typeof result !== 'string') { - const error = new IntlError( - IntlErrorCode.FORMATTING_ERROR, - process.env.NODE_ENV !== 'production' - ? "`createTranslator` only accepts functions for rich text formatting that receive and return strings.\n\nE.g. t.rich('rich', {b: (chunks) => `${chunks}`})" - : undefined - ); - - onError(error); - return getMessageFallback({error, key, namespace}); - } - - return result; - }; - - base.raw = translator.raw; - - return base; } diff --git a/packages/use-intl/src/core/index.tsx b/packages/use-intl/src/core/index.tsx index 6bbcecc8f..b55e5f17e 100644 --- a/packages/use-intl/src/core/index.tsx +++ b/packages/use-intl/src/core/index.tsx @@ -1,14 +1,18 @@ export type {default as AbstractIntlMessages} from './AbstractIntlMessages'; export type { default as TranslationValues, - RichTranslationValues + RichTranslationValues, + MarkupTranslationValues } from './TranslationValues'; export type {default as Formats} from './Formats'; +export type {default as IntlConfig} from './IntlConfig'; export type {default as DateTimeFormatOptions} from './DateTimeFormatOptions'; export type {default as NumberFormatOptions} from './NumberFormatOptions'; export {default as IntlError, IntlErrorCode} from './IntlError'; export {default as createTranslator} from './createTranslator'; export {default as createFormatter} from './createFormatter'; - -// TODO: Remove in next major version -export {default as createIntl} from './createIntl'; +export {default as initializeConfig} from './initializeConfig'; +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'; diff --git a/packages/use-intl/src/react/getInitializedConfig.tsx b/packages/use-intl/src/core/initializeConfig.tsx similarity index 74% rename from packages/use-intl/src/react/getInitializedConfig.tsx rename to packages/use-intl/src/core/initializeConfig.tsx index 88f08b77f..854a1371f 100644 --- a/packages/use-intl/src/react/getInitializedConfig.tsx +++ b/packages/use-intl/src/core/initializeConfig.tsx @@ -1,11 +1,11 @@ -import IntlConfig from '../core/IntlConfig'; -import {defaultGetMessageFallback, defaultOnError} from '../core/defaults'; -import validateMessages from '../core/validateMessages'; +import IntlConfig from './IntlConfig'; +import {defaultGetMessageFallback, defaultOnError} from './defaults'; +import validateMessages from './validateMessages'; /** * Enhances the incoming props with defaults. */ -export default function getInitializedConfig< +export default function initializeConfig< // This is a generic to allow for stricter typing. E.g. // the RSC integration always provides a `now` value. Props extends Omit diff --git a/packages/use-intl/src/react.tsx b/packages/use-intl/src/react.tsx new file mode 100644 index 000000000..f97dcae79 --- /dev/null +++ b/packages/use-intl/src/react.tsx @@ -0,0 +1 @@ +export * from './react/index'; diff --git a/packages/use-intl/src/react/IntlProvider.tsx b/packages/use-intl/src/react/IntlProvider.tsx index f048ded42..22445b512 100644 --- a/packages/use-intl/src/react/IntlProvider.tsx +++ b/packages/use-intl/src/react/IntlProvider.tsx @@ -1,23 +1,59 @@ -import React, {ReactNode, useState} from 'react'; +import React, {ReactNode, useMemo, useState} from 'react'; import IntlConfig from '../core/IntlConfig'; +import initializeConfig from '../core/initializeConfig'; import IntlContext from './IntlContext'; -import getInitializedConfig from './getInitializedConfig'; type Props = IntlConfig & { children: ReactNode; }; -export default function IntlProvider({children, ...props}: Props) { +export default function IntlProvider({ + children, + defaultTranslationValues, + formats, + getMessageFallback, + locale, + messages, + now, + onError, + timeZone +}: Props) { const [messageFormatCache] = useState(() => new Map()); - return ( - - {children} - + // Memoizing this value helps to avoid triggering a re-render of all + // context consumers in case the configuration didn't change. However, + // if some of the non-primitive values change, a re-render will still + // be triggered. Note that there's no need to put `memo` on `IntlProvider` + // itself, because the `children` typically change on every render. + // There's some burden on the consumer side if it's important to reduce + // re-renders, put that's how React works. + // See: https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior/#context-updates-and-render-optimizations + const value = useMemo( + () => ({ + ...initializeConfig({ + locale, + defaultTranslationValues, + formats, + getMessageFallback, + messages, + now, + onError, + timeZone + }), + messageFormatCache + }), + [ + defaultTranslationValues, + formats, + getMessageFallback, + locale, + messageFormatCache, + messages, + now, + onError, + timeZone + ] ); + + return {children}; } 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 0dcbbf27b..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 (!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/useMessages.tsx b/packages/use-intl/src/react/useMessages.tsx index 19ae778df..24a9d0481 100644 --- a/packages/use-intl/src/react/useMessages.tsx +++ b/packages/use-intl/src/react/useMessages.tsx @@ -1,5 +1,16 @@ +import {AbstractIntlMessages} from '../core'; import useIntlContext from './useIntlContext'; -export default function useMessages() { - return useIntlContext().messages; +export default function useMessages(): AbstractIntlMessages { + const context = useIntlContext(); + + if (!context.messages) { + throw new Error( + process.env.NODE_ENV !== 'production' + ? 'No messages found. Have you configured them correctly? See https://next-intl-docs.vercel.app/docs/configuration#messages' + : undefined + ); + } + + return context.messages; } diff --git a/packages/use-intl/src/react/useTranslations.tsx b/packages/use-intl/src/react/useTranslations.tsx index a4cd654a8..729bfacf6 100644 --- a/packages/use-intl/src/react/useTranslations.tsx +++ b/packages/use-intl/src/react/useTranslations.tsx @@ -1,6 +1,7 @@ import {ReactElement, ReactNodeArray} from 'react'; import Formats from '../core/Formats'; import TranslationValues, { + MarkupTranslationValues, RichTranslationValues } from '../core/TranslationValues'; import MessageKeys from '../core/utils/MessageKeys'; @@ -67,6 +68,26 @@ export default function useTranslations< 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< diff --git a/packages/use-intl/src/react/useTranslationsImpl.tsx b/packages/use-intl/src/react/useTranslationsImpl.tsx index afbd67fb2..80dc55207 100644 --- a/packages/use-intl/src/react/useTranslationsImpl.tsx +++ b/packages/use-intl/src/react/useTranslationsImpl.tsx @@ -1,12 +1,14 @@ import {useMemo} from 'react'; +import {IntlError, IntlErrorCode} from '../core'; import AbstractIntlMessages from '../core/AbstractIntlMessages'; -import createBaseTranslator, { - getMessagesOrError -} from '../core/createBaseTranslator'; +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 @@ -26,17 +28,24 @@ export default function useTranslationsImpl< allMessages = allMessages[namespacePrefix] as Messages; namespace = resolveNamespace(namespace, namespacePrefix) as NestedKey; - const messagesOrError = useMemo( - () => getMessagesOrError({messages: allMessages, namespace, onError}), - [allMessages, namespace, onError] - ); + 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({ messageFormatCache, getMessageFallback, - messagesOrError, + messages: allMessages, defaultTranslationValues, namespace, onError, @@ -47,10 +56,10 @@ export default function useTranslationsImpl< [ messageFormatCache, getMessageFallback, - messagesOrError, - defaultTranslationValues, + allMessages, namespace, onError, + defaultTranslationValues, globalFormats, locale, timeZone 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/core/createTranslator.test.tsx b/packages/use-intl/test/core/createTranslator.test.tsx index 357048ed2..3d9030d96 100644 --- a/packages/use-intl/test/core/createTranslator.test.tsx +++ b/packages/use-intl/test/core/createTranslator.test.tsx @@ -1,11 +1,13 @@ -import React from 'react'; +import React, {isValidElement} from 'react'; +import {renderToString} from 'react-dom/server'; import {it, expect, describe, vi} from 'vitest'; import {createTranslator, IntlError, IntlErrorCode} from '../../src'; const messages = { Home: { title: 'Hello world!', - rich: 'Hello {name}!' + rich: 'Hello {name}!', + markup: 'Hello {name}!' } }; @@ -48,6 +50,25 @@ it('handles formatting errors', () => { }); describe('t.rich', () => { + it('can translate a message to a ReactNode', () => { + const t = createTranslator({ + locale: 'en', + namespace: 'Home', + messages + }); + + const result = t.rich('rich', { + name: 'world', + b: (chunks) => {chunks}, + i: (chunks) => {chunks} + }); + + expect(isValidElement(result)).toBe(true); + expect(renderToString(result as any)).toBe('Hello world!'); + }); +}); + +describe('t.markup', () => { it('can translate a message', () => { const t = createTranslator({ locale: 'en', @@ -56,7 +77,7 @@ describe('t.rich', () => { }); expect( - t.rich('rich', { + t.markup('markup', { name: 'world', b: (chunks) => `${chunks}`, i: (chunks) => `${chunks}` @@ -66,6 +87,7 @@ describe('t.rich', () => { it('handles errors when React components are provided', () => { const onError = vi.fn(); + const t = createTranslator({ locale: 'en', namespace: 'Home', @@ -73,7 +95,7 @@ describe('t.rich', () => { onError }); - const result = t.rich('rich', { + const result = t.markup('markup', { name: 'world', // @ts-expect-error Intentionally broken call site b: (chunks) => {chunks}, @@ -85,9 +107,9 @@ describe('t.rich', () => { const error: IntlError = onError.mock.calls[0][0]; expect(error.code).toBe(IntlErrorCode.FORMATTING_ERROR); expect(error.message).toBe( - "FORMATTING_ERROR: `createTranslator` only accepts functions for rich text formatting that receive and return strings.\n\nE.g. t.rich('rich', {b: (chunks) => `${chunks}`})" + "FORMATTING_ERROR: `t.markup` only accepts functions for formatting that receive and return strings.\n\nE.g. t.markup('markup', {b: (chunks) => `${chunks}`})" ); - expect(result).toBe('Home.rich'); + expect(result).toBe('Home.markup'); }); }); diff --git a/packages/use-intl/test/react/IntlProvider.test.tsx b/packages/use-intl/test/react/IntlProvider.test.tsx new file mode 100644 index 000000000..e21dd27c1 --- /dev/null +++ b/packages/use-intl/test/react/IntlProvider.test.tsx @@ -0,0 +1,44 @@ +import {fireEvent, render, screen} from '@testing-library/react'; +import React, {memo, useState} from 'react'; +import {expect, it} from 'vitest'; +import {IntlProvider, useTranslations} from '../../src'; + +it("doesn't re-render context consumers unnecessarily", () => { + const messages = {StaticText: {hello: 'Hello!'}}; + + let numCounterRenders = 0; + function Counter() { + const [count, setCount] = useState(0); + numCounterRenders++; + + return ( + <> + +

Count: {count}

+ + + + + ); + } + + // `memo` is required on this component, as otherwise + // React doesn't bail out of rendering it. + let numStaticTextRenders = 0; + const StaticText = memo(() => { + const t = useTranslations('StaticText'); + numStaticTextRenders++; + return t('hello'); + }); + + render(); + screen.getByText('Count: 0'); + expect(numCounterRenders).toBe(1); + expect(numStaticTextRenders).toBe(1); + fireEvent.click(screen.getByText('Increment')); + screen.getByText('Count: 1'); + expect(numCounterRenders).toBe(2); + expect(numStaticTextRenders).toBe(1); +}); 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); - }); - }); -}); diff --git a/packages/use-intl/test/react/useMessages.test.tsx b/packages/use-intl/test/react/useMessages.test.tsx index e12409a5a..4b46509a9 100644 --- a/packages/use-intl/test/react/useMessages.test.tsx +++ b/packages/use-intl/test/react/useMessages.test.tsx @@ -3,11 +3,20 @@ import React from 'react'; import {it, expect} from 'vitest'; import {IntlProvider, useMessages} from '../../src'; -it('returns messages when they are configured', () => { - function Component() { - return <>{JSON.stringify(useMessages())}; - } +function Component() { + const messages = useMessages(); + return ( + <> + {JSON.stringify(messages)} + {/* The returned value can be passed to the provider */} + +

+ + + ); +} +it('returns messages when they are configured', () => { render( @@ -17,16 +26,12 @@ it('returns messages when they are configured', () => { screen.getByText('{"About":{"title":"Hello"}}'); }); -it('returns undefined when no messages are configured', () => { - function Component() { - return <>{useMessages()}; - } - - const {container} = render( - - - - ); - - expect(container.textContent).toBe(''); +it('throws when no messages are configured', () => { + expect(() => + render( + + + + ) + ).toThrow('No messages found.'); }); diff --git a/packages/use-intl/test/react/useTranslations.test.tsx b/packages/use-intl/test/react/useTranslations.test.tsx index 09ff56957..66df69b80 100644 --- a/packages/use-intl/test/react/useTranslations.test.tsx +++ b/packages/use-intl/test/react/useTranslations.test.tsx @@ -439,6 +439,35 @@ describe('t.rich', () => { }); }); +describe('t.markup', () => { + it('returns markup text', () => { + let result; + + function Component() { + const t = useTranslations(); + result = t.markup('message', { + important: (children) => `${children}` + }); + return null; + } + + render( + important and this as well' + }} + timeZone="Europe/London" + > + + + ); + + expect(result).toBe('This is important and this as well'); + }); +}); + describe('t.raw', () => { function renderRawMessage( message: any, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc0e9c930..6cdf78d32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -8,9 +8,45 @@ importers: .: devDependencies: + '@babel/core': + specifier: ^7.12.9 + version: 7.22.5 + '@babel/preset-env': + specifier: ^7.22.14 + version: 7.22.14(@babel/core@7.22.5) + '@babel/preset-react': + specifier: ^7.22.5 + version: 7.22.15(@babel/core@7.22.5) + '@babel/preset-typescript': + specifier: ^7.22.11 + version: 7.22.11(@babel/core@7.22.5) + '@rollup/plugin-babel': + specifier: ^6.0.3 + version: 6.0.4(@babel/core@7.22.5)(rollup@3.28.1) + '@rollup/plugin-commonjs': + specifier: ^25.0.4 + version: 25.0.7(rollup@3.28.1) + '@rollup/plugin-node-resolve': + specifier: ^15.2.1 + version: 15.2.3(rollup@3.28.1) + '@rollup/plugin-replace': + specifier: ^5.0.2 + version: 5.0.5(rollup@3.28.1) + '@rollup/plugin-terser': + specifier: ^0.4.3 + version: 0.4.4(rollup@3.28.1) + execa: + specifier: ^5.1.1 + version: 5.1.1 lerna: specifier: ^6.6.2 version: 6.6.2 + rollup: + specifier: ^3.28.1 + version: 3.28.1 + tsup: + specifier: ^7.2.0 + version: 7.2.0 turbo: specifier: ^1.9.3 version: 1.9.3 @@ -52,11 +88,11 @@ importers: version: 3.3.2 devDependencies: '@types/node': - specifier: 20.1.2 + specifier: ^20.1.2 version: 20.1.2 '@types/react': - specifier: ^18.2.5 - version: 18.2.5 + specifier: ^18.2.29 + version: 18.2.34 autoprefixer: specifier: ^10.4.0 version: 10.4.0(postcss@8.4.31) @@ -65,16 +101,16 @@ importers: version: 8.46.0 eslint-config-molindo: specifier: ^7.0.0 - version: 7.0.0(eslint@8.46.0)(jest@27.5.1)(tailwindcss@3.3.3)(typescript@5.0.4) + version: 7.0.0(eslint@8.46.0)(jest@29.5.0)(tailwindcss@3.3.2)(typescript@5.2.2) eslint-config-next: specifier: ^13.4.0 - version: 13.4.0(eslint@8.46.0)(typescript@5.0.4) + version: 13.4.0(eslint@8.46.0)(typescript@5.2.2) next-sitemap: specifier: ^4.0.7 version: 4.0.7(@next/env@14.0.1)(next@14.0.1) typescript: - specifier: ^5.0.0 - version: 5.0.4 + specifier: ^5.2.2 + version: 5.2.2 examples/example: dependencies: @@ -101,20 +137,20 @@ importers: specifier: ^17.0.23 version: 17.0.23 '@types/react': - specifier: ^18.2.5 - version: 18.2.5 + specifier: ^18.2.29 + version: 18.2.34 eslint: specifier: ^8.46.0 version: 8.46.0 eslint-config-molindo: specifier: ^7.0.0 - version: 7.0.0(eslint@8.46.0)(jest@27.5.1)(tailwindcss@3.3.3)(typescript@5.0.4) + version: 7.0.0(eslint@8.46.0)(jest@29.5.0)(tailwindcss@3.3.3)(typescript@5.2.2) eslint-config-next: specifier: ^13.4.0 - version: 13.4.0(eslint@8.46.0)(typescript@5.0.4) + version: 13.4.0(eslint@8.46.0)(typescript@5.2.2) typescript: - specifier: ^5.0.0 - version: 5.0.4 + specifier: ^5.2.2 + version: 5.2.2 examples/example-advanced: dependencies: @@ -153,26 +189,26 @@ importers: specifier: ^17.0.23 version: 17.0.23 '@types/react': - specifier: ^18.2.5 - version: 18.2.5 + specifier: ^18.2.29 + version: 18.2.34 eslint: specifier: ^8.46.0 version: 8.46.0 eslint-config-molindo: specifier: ^7.0.0 - version: 7.0.0(eslint@8.46.0)(jest@27.4.5)(tailwindcss@3.3.3)(typescript@5.0.4) + version: 7.0.0(eslint@8.46.0)(jest@29.5.0)(tailwindcss@3.3.3)(typescript@5.2.2) eslint-config-next: specifier: ^13.4.0 - version: 13.4.0(eslint@8.46.0)(typescript@5.0.4) + version: 13.4.0(eslint@8.46.0)(typescript@5.2.2) jest: - specifier: ^27.4.5 - version: 27.4.5 + specifier: ^29.0.0 + version: 29.5.0(@types/node@17.0.23) jest-environment-jsdom: - specifier: ^27.0.0 - version: 27.0.0 + specifier: ^29.0.0 + version: 29.5.0 typescript: - specifier: ^5.0.0 - version: 5.0.4 + specifier: ^5.2.2 + version: 5.2.2 examples/example-next-13: dependencies: @@ -199,7 +235,7 @@ importers: specifier: ^29.5.0 version: 29.5.0 '@playwright/test': - specifier: ^1.28.1 + specifier: ^1.33.0 version: 1.33.0 '@testing-library/react': specifier: ^13.0.0 @@ -214,8 +250,8 @@ importers: specifier: ^17.0.23 version: 17.0.23 '@types/react': - specifier: ^18.2.5 - version: 18.2.5 + specifier: ^18.2.29 + version: 18.2.34 autoprefixer: specifier: ^10.4.0 version: 10.4.0(postcss@8.4.23) @@ -224,10 +260,10 @@ importers: version: 8.46.0 eslint-config-molindo: specifier: ^7.0.0 - version: 7.0.0(eslint@8.46.0)(jest@29.5.0)(tailwindcss@3.3.2)(typescript@5.0.4) + version: 7.0.0(eslint@8.46.0)(jest@29.5.0)(tailwindcss@3.3.2)(typescript@5.2.2) eslint-config-next: specifier: ^13.4.0 - version: 13.4.0(eslint@8.46.0)(typescript@5.0.4) + version: 13.4.0(eslint@8.46.0)(typescript@5.2.2) jest: specifier: ^29.5.0 version: 29.5.0(@types/node@17.0.23) @@ -241,8 +277,60 @@ importers: specifier: ^0.2.3 version: 0.2.3(prettier@3.0.2) typescript: - specifier: ^5.0.0 - version: 5.0.4 + specifier: ^5.2.2 + version: 5.2.2 + + examples/example-next-13-advanced: + dependencies: + lodash: + specifier: ^4.17.21 + version: 4.17.21 + ms: + specifier: 2.1.3 + version: 2.1.3 + next: + specifier: 14.0.1 + version: 14.0.1(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + next-intl: + specifier: ^2.13.2 + version: link:../../packages/next-intl + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + devDependencies: + '@playwright/test': + specifier: ^1.33.0 + version: 1.33.0 + '@types/lodash': + specifier: ^4.14.176 + version: 4.14.176 + '@types/node': + specifier: ^17.0.23 + version: 17.0.23 + '@types/react': + specifier: ^18.2.29 + version: 18.2.34 + chokidar-cli: + specifier: 3.0.0 + version: 3.0.0 + eslint: + specifier: ^8.46.0 + version: 8.46.0 + eslint-config-molindo: + specifier: 7.0.0-alpha.7 + version: 7.0.0-alpha.7(eslint@8.46.0)(tailwindcss@3.3.3)(typescript@5.2.2) + eslint-config-next: + specifier: ^13.4.0 + version: 13.4.0(eslint@8.46.0)(typescript@5.2.2) + sharp: + specifier: ^0.32.6 + version: 0.32.6 + typescript: + specifier: ^5.2.2 + version: 5.2.2 examples/example-next-13-next-auth: dependencies: @@ -263,7 +351,7 @@ importers: version: 18.2.0(react@18.2.0) devDependencies: '@playwright/test': - specifier: ^1.28.1 + specifier: ^1.33.0 version: 1.33.0 '@types/lodash': specifier: ^4.14.176 @@ -272,26 +360,63 @@ importers: specifier: ^17.0.23 version: 17.0.23 '@types/react': - specifier: ^18.2.5 - version: 18.2.5 + specifier: ^18.2.29 + version: 18.2.34 eslint: specifier: ^8.46.0 version: 8.46.0 eslint-config-molindo: specifier: ^7.0.0 - version: 7.0.0(eslint@8.46.0)(jest@27.5.1)(tailwindcss@3.3.3)(typescript@5.0.4) + version: 7.0.0(eslint@8.46.0)(jest@29.5.0)(tailwindcss@3.3.3)(typescript@5.2.2) + eslint-config-next: + specifier: ^13.4.0 + version: 13.4.0(eslint@8.46.0)(typescript@5.2.2) + typescript: + specifier: ^5.2.2 + version: 5.2.2 + + examples/example-next-13-with-pages: + dependencies: + next: + specifier: 14.0.1 + version: 14.0.1(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + next-intl: + specifier: ^2.13.2 + version: link:../../packages/next-intl + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + devDependencies: + '@types/lodash': + specifier: ^4.14.176 + version: 4.14.176 + '@types/node': + specifier: ^17.0.23 + version: 17.0.23 + '@types/react': + specifier: ^18.2.29 + version: 18.2.34 + eslint: + specifier: ^8.46.0 + version: 8.46.0 + eslint-config-molindo: + specifier: 7.0.0-alpha.7 + version: 7.0.0-alpha.7(eslint@8.46.0)(tailwindcss@3.3.3)(typescript@5.2.2) eslint-config-next: specifier: ^13.4.0 - version: 13.4.0(eslint@8.46.0)(typescript@5.0.4) + version: 13.4.0(eslint@8.46.0)(typescript@5.2.2) typescript: - specifier: ^5.0.0 - version: 5.0.4 + specifier: ^5.2.2 + version: 5.2.2 examples/example-react-native: dependencies: '@expo/webpack-config': specifier: ^0.17.2 - version: 0.17.2(expo@47.0.12)(typescript@5.0.4) + version: 0.17.2(expo@47.0.12)(typescript@5.2.2) expo: specifier: ~47.0.12 version: 47.0.12(@babel/core@7.21.8) @@ -349,8 +474,8 @@ importers: specifier: ^1.5.3 version: 1.5.3 '@types/react': - specifier: ^18.2.5 - version: 18.2.5 + specifier: ^18.2.29 + version: 18.2.34 '@types/react-dom': specifier: ^18.2.1 version: 18.2.2 @@ -359,10 +484,10 @@ importers: version: 8.46.0 eslint-config-molindo: specifier: ^7.0.0 - version: 7.0.0(eslint@8.46.0)(jest@27.5.1)(tailwindcss@3.3.3)(typescript@5.0.4) + version: 7.0.0(eslint@8.46.0)(jest@29.5.0)(tailwindcss@3.3.3)(typescript@5.2.2) typescript: - specifier: ^5.0.0 - version: 5.0.4 + specifier: ^5.2.2 + version: 5.2.2 packages/next-intl: dependencies: @@ -381,7 +506,7 @@ importers: version: 3.1.3 '@size-limit/preset-big-lib': specifier: ^8.2.6 - version: 8.2.6(size-limit@8.2.6) + version: 8.2.6(esbuild@0.18.20)(size-limit@8.2.6) '@testing-library/react': specifier: ^13.0.0 version: 13.0.0(react-dom@18.2.0)(react@18.2.0) @@ -392,20 +517,17 @@ importers: specifier: ^17.0.23 version: 17.0.23 '@types/react': - specifier: ^18.2.5 - version: 18.2.5 - dts-cli: - specifier: ^1.4.0 - version: 1.4.0(@babel/plugin-syntax-flow@7.21.4)(@babel/plugin-transform-react-jsx@7.22.5)(@types/node@17.0.23) + specifier: 18.2.34 + version: 18.2.34 eslint: specifier: ^8.46.0 version: 8.46.0 eslint-config-molindo: specifier: ^7.0.0 - version: 7.0.0(eslint@8.46.0)(jest@27.5.1)(tailwindcss@3.3.3)(typescript@5.0.4) + version: 7.0.0(eslint@8.46.0)(jest@29.5.0)(tailwindcss@3.3.3)(typescript@5.2.2) eslint-plugin-deprecation: specifier: ^1.4.1 - version: 1.4.1(eslint@8.46.0)(typescript@5.0.4) + version: 1.4.1(eslint@8.46.0)(typescript@5.2.2) next: specifier: 14.0.1 version: 14.0.1(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) @@ -418,12 +540,18 @@ importers: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + rollup: + specifier: ^3.28.1 + version: 3.28.1 + rollup-plugin-preserve-directives: + specifier: 0.2.0 + version: 0.2.0(rollup@3.28.1) size-limit: specifier: ^8.2.6 version: 8.2.6 typescript: - specifier: ^5.0.0 - version: 5.0.4 + specifier: ^5.2.2 + version: 5.2.2 vitest: specifier: ^0.32.2 version: 0.32.2(@edge-runtime/vm@3.1.3) @@ -439,7 +567,7 @@ importers: devDependencies: '@size-limit/preset-big-lib': specifier: ^8.2.6 - version: 8.2.6(size-limit@8.2.6) + version: 8.2.6(esbuild@0.18.20)(size-limit@8.2.6) '@testing-library/react': specifier: ^13.0.0 version: 13.0.0(react-dom@18.2.0)(react@18.2.0) @@ -447,32 +575,35 @@ importers: specifier: ^17.0.23 version: 17.0.23 '@types/react': + specifier: ^18.2.29 + version: 18.2.34 + '@types/react-dom': specifier: ^18.2.5 - version: 18.2.5 + version: 18.2.14 date-fns: specifier: ^2.16.1 version: 2.16.1 - dts-cli: - specifier: ^1.4.0 - version: 1.4.0(@babel/plugin-syntax-flow@7.21.4)(@babel/plugin-transform-react-jsx@7.22.5)(@types/node@17.0.23) eslint: specifier: ^8.46.0 version: 8.46.0 eslint-config-molindo: specifier: ^7.0.0 - version: 7.0.0(eslint@8.46.0)(jest@27.5.1)(tailwindcss@3.3.3)(typescript@5.0.4) + version: 7.0.0(eslint@8.46.0)(jest@29.5.0)(tailwindcss@3.3.3)(typescript@5.2.2) react: specifier: ^18.2.0 version: 18.2.0 react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + rollup: + specifier: ^3.28.1 + version: 3.28.1 size-limit: specifier: ^8.2.6 version: 8.2.6 typescript: - specifier: ^5.0.0 - version: 5.0.4 + specifier: ^5.2.2 + version: 5.2.2 vitest: specifier: ^0.32.2 version: 0.32.2(@edge-runtime/vm@3.1.3) @@ -708,6 +839,7 @@ packages: semver: 6.3.1 transitivePeerDependencies: - supports-color + dev: false /@babel/helper-define-polyfill-provider@0.4.2(@babel/core@7.21.8): resolution: {integrity: sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==} @@ -762,6 +894,13 @@ packages: dependencies: '@babel/types': 7.22.11 + /@babel/helper-module-imports@7.22.15: + resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + /@babel/helper-module-imports@7.22.5: resolution: {integrity: sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==} engines: {node: '>=6.9.0'} @@ -886,10 +1025,20 @@ packages: resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} engines: {node: '>=6.9.0'} + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/helper-validator-identifier@7.22.5: resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} engines: {node: '>=6.9.0'} + /@babel/helper-validator-option@7.22.15: + resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/helper-validator-option@7.22.5: resolution: {integrity: sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==} engines: {node: '>=6.9.0'} @@ -1027,6 +1176,7 @@ packages: '@babel/core': 7.22.5 '@babel/helper-create-class-features-plugin': 7.22.11(@babel/core@7.22.5) '@babel/helper-plugin-utils': 7.22.5 + dev: false /@babel/plugin-proposal-decorators@7.21.0(@babel/core@7.21.8): resolution: {integrity: sha512-MfgX49uRrFUTL/HvWtmx3zmpyzMMr4MTj3d527MLlr/4RTT9G/ytFFP7qet2uM2Ve03b+BkpWUpK+lRXnQ+v9w==} @@ -1324,6 +1474,7 @@ packages: dependencies: '@babel/core': 7.22.5 '@babel/helper-plugin-utils': 7.22.5 + dev: false /@babel/plugin-syntax-import-assertions@7.22.5(@babel/core@7.21.8): resolution: {integrity: sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==} @@ -2482,7 +2633,16 @@ packages: dependencies: '@babel/core': 7.22.5 '@babel/helper-plugin-utils': 7.22.5 - dev: false + + /@babel/plugin-transform-react-jsx-development@7.22.5(@babel/core@7.22.5): + resolution: {integrity: sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.5 + '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.22.5) + dev: true /@babel/plugin-transform-react-jsx-self@7.21.0(@babel/core@7.21.8): resolution: {integrity: sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA==} @@ -2524,6 +2684,20 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: false + /@babel/plugin-transform-react-jsx@7.22.15(@babel/core@7.22.5): + resolution: {integrity: sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.5 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.22.5) + '@babel/types': 7.23.0 + dev: true + /@babel/plugin-transform-react-jsx@7.22.5(@babel/core@7.21.8): resolution: {integrity: sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA==} engines: {node: '>=6.9.0'} @@ -2550,6 +2724,18 @@ packages: '@babel/helper-plugin-utils': 7.22.5 '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.22.5) '@babel/types': 7.22.11 + dev: false + + /@babel/plugin-transform-react-pure-annotations@7.22.5(@babel/core@7.22.5): + resolution: {integrity: sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.5 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: true /@babel/plugin-transform-regenerator@7.22.10(@babel/core@7.21.8): resolution: {integrity: sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==} @@ -3051,6 +3237,21 @@ packages: esutils: 2.0.3 dev: true + /@babel/preset-react@7.22.15(@babel/core@7.22.5): + resolution: {integrity: sha512-Csy1IJ2uEh/PecCBXXoZGAZBeCATTuePzCSB7dLYWS0vOEj6CNpjxIhW4duWwZodBNueH7QO14WbGn8YyeuN9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 + '@babel/plugin-transform-react-display-name': 7.22.5(@babel/core@7.22.5) + '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.22.5) + '@babel/plugin-transform-react-jsx-development': 7.22.5(@babel/core@7.22.5) + '@babel/plugin-transform-react-pure-annotations': 7.22.5(@babel/core@7.22.5) + dev: true + /@babel/preset-typescript@7.22.11(@babel/core@7.22.5): resolution: {integrity: sha512-tWY5wyCZYBGY7IlalfKI1rLiGlIfnwsRHZqlky0HVv8qviwQ1Uo/05M6+s+TcTCVa6Bmoo2uJW5TMFX6Wa4qVg==} engines: {node: '>=6.9.0'} @@ -3147,6 +3348,15 @@ packages: '@babel/helper-validator-identifier': 7.22.5 to-fast-properties: 2.0.0 + /@babel/types@7.23.0: + resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.22.5 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + /@bcoe/v8-coverage@0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true @@ -3159,13 +3369,6 @@ packages: resolution: {integrity: sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==} dev: true - /@cspotcode/source-map-support@0.8.1: - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - dev: true - /@edge-runtime/primitives@4.0.1: resolution: {integrity: sha512-hxWUzx1SeyOed/Ea9Z6y6tyFKSj8gQWIdLilybTR2ene1IthLZE01A1SLGoch1szUdhFlUwpWDaYBYQw00lj2g==} engines: {node: '>=16'} @@ -3219,6 +3422,15 @@ packages: dev: true optional: true + /@esbuild/android-arm64@0.18.20: + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm@0.16.3: resolution: {integrity: sha512-mueuEoh+s1eRbSJqq9KNBQwI4QhQV6sRXIfTyLXSHGMpyew61rOK4qY21uKbXl1iBoMb0AdL1deWFCQVlN2qHA==} engines: {node: '>=12'} @@ -3246,6 +3458,15 @@ packages: dev: true optional: true + /@esbuild/android-arm@0.18.20: + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-x64@0.16.3: resolution: {integrity: sha512-SFpTUcIT1bIJuCCBMCQWq1bL2gPTjWoLZdjmIhjdcQHaUfV41OQfho6Ici5uvvkMmZRXIUGpM3GxysP/EU7ifQ==} engines: {node: '>=12'} @@ -3273,6 +3494,15 @@ packages: dev: true optional: true + /@esbuild/android-x64@0.18.20: + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-arm64@0.16.3: resolution: {integrity: sha512-DO8WykMyB+N9mIDfI/Hug70Dk1KipavlGAecxS3jDUwAbTpDXj0Lcwzw9svkhxfpCagDmpaTMgxWK8/C/XcXvw==} engines: {node: '>=12'} @@ -3300,6 +3530,15 @@ packages: dev: true optional: true + /@esbuild/darwin-arm64@0.18.20: + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-x64@0.16.3: resolution: {integrity: sha512-uEqZQ2omc6BvWqdCiyZ5+XmxuHEi1SPzpVxXCSSV2+Sh7sbXbpeNhHIeFrIpRjAs0lI1FmA1iIOxFozKBhKgRQ==} engines: {node: '>=12'} @@ -3327,6 +3566,15 @@ packages: dev: true optional: true + /@esbuild/darwin-x64@0.18.20: + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-arm64@0.16.3: resolution: {integrity: sha512-nJansp3sSXakNkOD5i5mIz2Is/HjzIhFs49b1tjrPrpCmwgBmH9SSzhC/Z1UqlkivqMYkhfPwMw1dGFUuwmXhw==} engines: {node: '>=12'} @@ -3354,6 +3602,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-arm64@0.18.20: + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-x64@0.16.3: resolution: {integrity: sha512-TfoDzLw+QHfc4a8aKtGSQ96Wa+6eimljjkq9HKR0rHlU83vw8aldMOUSJTUDxbcUdcgnJzPaX8/vGWm7vyV7ug==} engines: {node: '>=12'} @@ -3381,6 +3638,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-x64@0.18.20: + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm64@0.16.3: resolution: {integrity: sha512-7I3RlsnxEFCHVZNBLb2w7unamgZ5sVwO0/ikE2GaYvYuUQs9Qte/w7TqWcXHtCwxvZx/2+F97ndiUQAWs47ZfQ==} engines: {node: '>=12'} @@ -3408,6 +3674,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm64@0.18.20: + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm@0.16.3: resolution: {integrity: sha512-VwswmSYwVAAq6LysV59Fyqk3UIjbhuc6wb3vEcJ7HEJUtFuLK9uXWuFoH1lulEbE4+5GjtHi3MHX+w1gNHdOWQ==} engines: {node: '>=12'} @@ -3435,6 +3710,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm@0.18.20: + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ia32@0.16.3: resolution: {integrity: sha512-X8FDDxM9cqda2rJE+iblQhIMYY49LfvW4kaEjoFbTTQ4Go8G96Smj2w3BRTwA8IHGoi9dPOPGAX63dhuv19UqA==} engines: {node: '>=12'} @@ -3462,6 +3746,15 @@ packages: dev: true optional: true + /@esbuild/linux-ia32@0.18.20: + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64@0.16.3: resolution: {integrity: sha512-hIbeejCOyO0X9ujfIIOKjBjNAs9XD/YdJ9JXAy1lHA+8UXuOqbFe4ErMCqMr8dhlMGBuvcQYGF7+kO7waj2KHw==} engines: {node: '>=12'} @@ -3489,6 +3782,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64@0.18.20: + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-mips64el@0.16.3: resolution: {integrity: sha512-znFRzICT/V8VZQMt6rjb21MtAVJv/3dmKRMlohlShrbVXdBuOdDrGb+C2cZGQAR8RFyRe7HS6klmHq103WpmVw==} engines: {node: '>=12'} @@ -3516,6 +3818,15 @@ packages: dev: true optional: true + /@esbuild/linux-mips64el@0.18.20: + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ppc64@0.16.3: resolution: {integrity: sha512-EV7LuEybxhXrVTDpbqWF2yehYRNz5e5p+u3oQUS2+ZFpknyi1NXxr8URk4ykR8Efm7iu04//4sBg249yNOwy5Q==} engines: {node: '>=12'} @@ -3543,6 +3854,15 @@ packages: dev: true optional: true + /@esbuild/linux-ppc64@0.18.20: + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-riscv64@0.16.3: resolution: {integrity: sha512-uDxqFOcLzFIJ+r/pkTTSE9lsCEaV/Y6rMlQjUI9BkzASEChYL/aSQjZjchtEmdnVxDKETnUAmsaZ4pqK1eE5BQ==} engines: {node: '>=12'} @@ -3570,6 +3890,15 @@ packages: dev: true optional: true + /@esbuild/linux-riscv64@0.18.20: + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-s390x@0.16.3: resolution: {integrity: sha512-NbeREhzSxYwFhnCAQOQZmajsPYtX71Ufej3IQ8W2Gxskfz9DK58ENEju4SbpIj48VenktRASC52N5Fhyf/aliQ==} engines: {node: '>=12'} @@ -3597,6 +3926,15 @@ packages: dev: true optional: true + /@esbuild/linux-s390x@0.18.20: + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-x64@0.16.3: resolution: {integrity: sha512-SDiG0nCixYO9JgpehoKgScwic7vXXndfasjnD5DLbp1xltANzqZ425l7LSdHynt19UWOcDjG9wJJzSElsPvk0w==} engines: {node: '>=12'} @@ -3624,6 +3962,15 @@ packages: dev: true optional: true + /@esbuild/linux-x64@0.18.20: + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-x64@0.16.3: resolution: {integrity: sha512-AzbsJqiHEq1I/tUvOfAzCY15h4/7Ivp3ff/o1GpP16n48JMNAtbW0qui2WCgoIZArEHD0SUQ95gvR0oSO7ZbdA==} engines: {node: '>=12'} @@ -3651,6 +3998,15 @@ packages: dev: true optional: true + /@esbuild/netbsd-x64@0.18.20: + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-x64@0.16.3: resolution: {integrity: sha512-gSABi8qHl8k3Cbi/4toAzHiykuBuWLZs43JomTcXkjMZVkp0gj3gg9mO+9HJW/8GB5H89RX/V0QP4JGL7YEEVg==} engines: {node: '>=12'} @@ -3678,8 +4034,17 @@ packages: dev: true optional: true - /@esbuild/sunos-x64@0.16.3: - resolution: {integrity: sha512-SF9Kch5Ete4reovvRO6yNjMxrvlfT0F0Flm+NPoUw5Z4Q3r1d23LFTgaLwm3Cp0iGbrU/MoUI+ZqwCv5XJijCw==} + /@esbuild/openbsd-x64@0.18.20: + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.16.3: + resolution: {integrity: sha512-SF9Kch5Ete4reovvRO6yNjMxrvlfT0F0Flm+NPoUw5Z4Q3r1d23LFTgaLwm3Cp0iGbrU/MoUI+ZqwCv5XJijCw==} engines: {node: '>=12'} cpu: [x64] os: [sunos] @@ -3705,6 +4070,15 @@ packages: dev: true optional: true + /@esbuild/sunos-x64@0.18.20: + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-arm64@0.16.3: resolution: {integrity: sha512-u5aBonZIyGopAZyOnoPAA6fGsDeHByZ9CnEzyML9NqntK6D/xl5jteZUKm/p6nD09+v3pTM6TuUIqSPcChk5gg==} engines: {node: '>=12'} @@ -3732,6 +4106,15 @@ packages: dev: true optional: true + /@esbuild/win32-arm64@0.18.20: + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-ia32@0.16.3: resolution: {integrity: sha512-GlgVq1WpvOEhNioh74TKelwla9KDuAaLZrdxuuUgsP2vayxeLgVc+rbpIv0IYF4+tlIzq2vRhofV+KGLD+37EQ==} engines: {node: '>=12'} @@ -3759,6 +4142,15 @@ packages: dev: true optional: true + /@esbuild/win32-ia32@0.18.20: + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-x64@0.16.3: resolution: {integrity: sha512-5/JuTd8OWW8UzEtyf19fbrtMJENza+C9JoPIkvItgTBQ1FO2ZLvjbPO6Xs54vk0s5JB5QsfieUEshRQfu7ZHow==} engines: {node: '>=12'} @@ -3786,6 +4178,15 @@ packages: dev: true optional: true + /@esbuild/win32-x64@0.18.20: + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.46.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4131,7 +4532,7 @@ packages: resolution: {integrity: sha512-TI+l71+5aSKnShYclFa14Kum+hQMZ86b95SH6tQUG3qZEmLTarvWpKwqtTwQKqvlJSJrpFiSFu3eCuZokY6zWA==} dev: false - /@expo/webpack-config@0.17.2(expo@47.0.12)(typescript@5.0.4): + /@expo/webpack-config@0.17.2(expo@47.0.12)(typescript@5.2.2): resolution: {integrity: sha512-cgcWyVXUEH5wj4InAPCIDHAGgpkQhpzWseCj4xVjdL3paBKRMWVjPUqmdHh/exap3U0kHGr/XS+e7ZWLcgHkUw==} engines: {node: '>=12'} dependencies: @@ -4153,9 +4554,9 @@ packages: mini-css-extract-plugin: 0.5.0(webpack@4.43.0) node-html-parser: 1.4.9 optimize-css-assets-webpack-plugin: 5.0.8(webpack@4.43.0) - pnp-webpack-plugin: 1.7.0(typescript@5.0.4) + pnp-webpack-plugin: 1.7.0(typescript@5.2.2) postcss-safe-parser: 4.0.2 - react-dev-utils: 11.0.4(typescript@5.0.4)(webpack@4.43.0) + react-dev-utils: 11.0.4(typescript@5.2.2)(webpack@4.43.0) schema-utils: 3.1.2 semver: 7.3.8 style-loader: 1.2.1(webpack@4.43.0) @@ -4311,75 +4712,18 @@ packages: engines: {node: '>=8'} dev: true - /@jest/console@27.5.1: - resolution: {integrity: sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/types': 27.5.1 - '@types/node': 17.0.23 - chalk: 4.1.2 - jest-message-util: 27.5.1 - jest-util: 27.5.1 - slash: 3.0.0 - dev: true - /@jest/console@29.5.0: resolution: {integrity: sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.5.0 - '@types/node': 17.0.23 + '@types/node': 20.1.2 chalk: 4.1.2 jest-message-util: 29.5.0 jest-util: 29.5.0 slash: 3.0.0 dev: true - /@jest/core@27.5.1(ts-node@10.9.1): - resolution: {integrity: sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/console': 27.5.1 - '@jest/reporters': 27.5.1 - '@jest/test-result': 27.5.1 - '@jest/transform': 27.5.1 - '@jest/types': 27.5.1 - '@types/node': 17.0.23 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - emittery: 0.8.1 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 27.5.1 - jest-config: 27.5.1(ts-node@10.9.1) - jest-haste-map: 27.5.1 - jest-message-util: 27.5.1 - jest-regex-util: 27.5.1 - jest-resolve: 27.5.1 - jest-resolve-dependencies: 27.5.1 - jest-runner: 27.5.1 - jest-runtime: 27.5.1 - jest-snapshot: 27.5.1 - jest-util: 27.5.1 - jest-validate: 27.5.1 - jest-watcher: 27.5.1 - micromatch: 4.0.5 - rimraf: 3.0.2 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - ts-node - - utf-8-validate - dev: true - /@jest/core@29.5.0: resolution: {integrity: sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4394,14 +4738,14 @@ packages: '@jest/test-result': 29.5.0 '@jest/transform': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 17.0.23 + '@types/node': 20.1.2 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.8.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.5.0 - jest-config: 29.5.0(@types/node@17.0.23) + jest-config: 29.5.0(@types/node@20.1.2) jest-haste-map: 29.5.0 jest-message-util: 29.5.0 jest-regex-util: 29.4.3 @@ -4429,23 +4773,13 @@ packages: '@jest/types': 29.5.0 dev: false - /@jest/environment@27.5.1: - resolution: {integrity: sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/fake-timers': 27.5.1 - '@jest/types': 27.5.1 - '@types/node': 17.0.23 - jest-mock: 27.5.1 - dev: true - /@jest/environment@29.5.0: resolution: {integrity: sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/fake-timers': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 17.0.23 + '@types/node': 20.1.2 jest-mock: 29.5.0 dev: true @@ -4466,39 +4800,18 @@ packages: - supports-color dev: true - /@jest/fake-timers@27.5.1: - resolution: {integrity: sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/types': 27.5.1 - '@sinonjs/fake-timers': 8.1.0 - '@types/node': 17.0.23 - jest-message-util: 27.5.1 - jest-mock: 27.5.1 - jest-util: 27.5.1 - dev: true - /@jest/fake-timers@29.5.0: resolution: {integrity: sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.5.0 '@sinonjs/fake-timers': 10.0.2 - '@types/node': 17.0.23 + '@types/node': 20.1.2 jest-message-util: 29.5.0 jest-mock: 29.5.0 jest-util: 29.5.0 dev: true - /@jest/globals@27.5.1: - resolution: {integrity: sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/environment': 27.5.1 - '@jest/types': 27.5.1 - expect: 27.5.1 - dev: true - /@jest/globals@29.5.0: resolution: {integrity: sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4511,44 +4824,6 @@ packages: - supports-color dev: true - /@jest/reporters@27.5.1: - resolution: {integrity: sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 27.5.1 - '@jest/test-result': 27.5.1 - '@jest/transform': 27.5.1 - '@jest/types': 27.5.1 - '@types/node': 17.0.23 - chalk: 4.1.2 - collect-v8-coverage: 1.0.1 - exit: 0.1.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - istanbul-lib-coverage: 3.2.0 - istanbul-lib-instrument: 5.2.1 - istanbul-lib-report: 3.0.0 - istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.1.5 - jest-haste-map: 27.5.1 - jest-resolve: 27.5.1 - jest-util: 27.5.1 - jest-worker: 27.5.1 - slash: 3.0.0 - source-map: 0.6.1 - string-length: 4.0.2 - terminal-link: 2.1.1 - v8-to-istanbul: 8.1.1 - transitivePeerDependencies: - - supports-color - dev: true - /@jest/reporters@29.5.0: resolution: {integrity: sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4564,7 +4839,7 @@ packages: '@jest/transform': 29.5.0 '@jest/types': 29.5.0 '@jridgewell/trace-mapping': 0.3.18 - '@types/node': 17.0.23 + '@types/node': 20.1.2 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -4592,15 +4867,6 @@ packages: dependencies: '@sinclair/typebox': 0.25.24 - /@jest/source-map@27.5.1: - resolution: {integrity: sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - callsites: 3.1.0 - graceful-fs: 4.2.11 - source-map: 0.6.1 - dev: true - /@jest/source-map@29.4.3: resolution: {integrity: sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4610,16 +4876,6 @@ packages: graceful-fs: 4.2.11 dev: true - /@jest/test-result@27.5.1: - resolution: {integrity: sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/console': 27.5.1 - '@jest/types': 27.5.1 - '@types/istanbul-lib-coverage': 2.0.4 - collect-v8-coverage: 1.0.1 - dev: true - /@jest/test-result@29.5.0: resolution: {integrity: sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4630,18 +4886,6 @@ packages: collect-v8-coverage: 1.0.1 dev: true - /@jest/test-sequencer@27.5.1: - resolution: {integrity: sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/test-result': 27.5.1 - graceful-fs: 4.2.11 - jest-haste-map: 27.5.1 - jest-runtime: 27.5.1 - transitivePeerDependencies: - - supports-color - dev: true - /@jest/test-sequencer@29.5.0: resolution: {integrity: sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4652,29 +4896,6 @@ packages: slash: 3.0.0 dev: true - /@jest/transform@27.5.1: - resolution: {integrity: sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@babel/core': 7.22.5 - '@jest/types': 27.5.1 - babel-plugin-istanbul: 6.1.1 - chalk: 4.1.2 - convert-source-map: 1.9.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.11 - jest-haste-map: 27.5.1 - jest-regex-util: 27.5.1 - jest-util: 27.5.1 - micromatch: 4.0.5 - pirates: 4.0.5 - slash: 3.0.0 - source-map: 0.6.1 - write-file-atomic: 3.0.3 - transitivePeerDependencies: - - supports-color - dev: true - /@jest/transform@29.5.0: resolution: {integrity: sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4704,7 +4925,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 17.0.23 + '@types/node': 20.1.2 '@types/yargs': 15.0.15 chalk: 4.1.2 dev: false @@ -4715,9 +4936,10 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 17.0.23 + '@types/node': 20.1.2 '@types/yargs': 16.0.5 chalk: 4.1.2 + dev: false /@jest/types@29.5.0: resolution: {integrity: sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==} @@ -4726,7 +4948,7 @@ packages: '@jest/schemas': 29.4.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 17.0.23 + '@types/node': 20.1.2 '@types/yargs': 17.0.24 chalk: 4.1.2 @@ -4765,13 +4987,6 @@ packages: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 - /@jridgewell/trace-mapping@0.3.9: - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true - /@lerna/child-process@6.6.2: resolution: {integrity: sha512-QyKIWEnKQFnYu2ey+SAAm1A5xjzJLJJj3bhIZd3QKyXKKjaJ0hlxam/OsWSltxTNbcyH1jRJjC6Cxv31usv0Ag==} engines: {node: ^14.17.0 || >=16.0.0} @@ -4906,7 +5121,7 @@ packages: react: '>=16' dependencies: '@types/mdx': 2.0.5 - '@types/react': 18.2.5 + '@types/react': 18.2.34 react: 18.2.0 dev: false @@ -5624,7 +5839,7 @@ packages: engines: {node: '>=14'} hasBin: true dependencies: - '@types/node': 17.0.23 + '@types/node': 20.1.2 playwright-core: 1.33.0 optionalDependencies: fsevents: 2.3.2 @@ -5998,7 +6213,7 @@ packages: dependencies: '@remix-run/router': 1.5.0 '@types/cookie': 0.4.1 - '@types/react': 18.2.5 + '@types/react': 18.2.34 '@web3-storage/multipart-parser': 1.0.0 cookie: 0.4.2 set-cookie-parser: 2.6.0 @@ -6037,83 +6252,88 @@ packages: dependencies: web-streams-polyfill: 3.2.1 - /@rollup/plugin-babel@5.3.1(@babel/core@7.22.5)(rollup@2.79.1): - resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} - engines: {node: '>= 10.0.0'} + /@rollup/plugin-babel@6.0.4(@babel/core@7.22.5)(rollup@3.28.1): + resolution: {integrity: sha512-YF7Y52kFdFT/xVSuVdjkV5ZdX/3YtmX0QulG+x0taQOtJdHYzVU61aSSkAgVJ7NOv6qPkIYiJSgSWWN/DM5sGw==} + engines: {node: '>=14.0.0'} peerDependencies: '@babel/core': ^7.0.0 '@types/babel__core': ^7.1.9 - rollup: ^1.20.0||^2.0.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: '@types/babel__core': optional: true + rollup: + optional: true dependencies: '@babel/core': 7.22.5 '@babel/helper-module-imports': 7.22.5 - '@rollup/pluginutils': 3.1.0(rollup@2.79.1) - rollup: 2.79.1 + '@rollup/pluginutils': 5.0.5(rollup@3.28.1) + rollup: 3.28.1 dev: true - /@rollup/plugin-commonjs@21.1.0(rollup@2.79.1): - resolution: {integrity: sha512-6ZtHx3VHIp2ReNNDxHjuUml6ur+WcQ28N1yHgCQwsbNkQg2suhxGMDQGJOn/KuDxKtd1xuZP5xSTwBA4GQ8hbA==} - engines: {node: '>= 8.0.0'} + /@rollup/plugin-commonjs@25.0.7(rollup@3.28.1): + resolution: {integrity: sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==} + engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^2.38.3 + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.79.1) + '@rollup/pluginutils': 5.0.5(rollup@3.28.1) commondir: 1.0.1 estree-walker: 2.0.2 - glob: 7.2.3 + glob: 8.1.0 is-reference: 1.2.1 - magic-string: 0.25.9 - resolve: 1.22.2 - rollup: 2.79.1 - dev: true - - /@rollup/plugin-json@4.1.0(rollup@2.79.1): - resolution: {integrity: sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==} - peerDependencies: - rollup: ^1.20.0 || ^2.0.0 - dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.79.1) - rollup: 2.79.1 + magic-string: 0.30.5 + rollup: 3.28.1 dev: true - /@rollup/plugin-node-resolve@13.3.0(rollup@2.79.1): - resolution: {integrity: sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==} - engines: {node: '>= 10.0.0'} + /@rollup/plugin-node-resolve@15.2.3(rollup@3.28.1): + resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} + engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^2.42.0 + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.79.1) - '@types/resolve': 1.17.1 + '@rollup/pluginutils': 5.0.5(rollup@3.28.1) + '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-builtin-module: 3.2.1 is-module: 1.0.0 resolve: 1.22.2 - rollup: 2.79.1 + rollup: 3.28.1 dev: true - /@rollup/plugin-replace@3.1.0(rollup@2.79.1): - resolution: {integrity: sha512-pA3XRUrSKybVYqmH5TqWNZpGxF+VV+1GrYchKgCNIj2vsSOX7CVm2RCtx8p2nrC7xvkziYyK+lSi74T93MU3YA==} + /@rollup/plugin-replace@5.0.5(rollup@3.28.1): + resolution: {integrity: sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==} + engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0 || ^2.0.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.79.1) - magic-string: 0.25.9 - rollup: 2.79.1 + '@rollup/pluginutils': 5.0.5(rollup@3.28.1) + magic-string: 0.30.5 + rollup: 3.28.1 dev: true - /@rollup/pluginutils@3.1.0(rollup@2.79.1): - resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} - engines: {node: '>= 8.0.0'} + /@rollup/plugin-terser@0.4.4(rollup@3.28.1): + resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} + engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0 + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true dependencies: - '@types/estree': 0.0.39 - estree-walker: 1.0.1 - picomatch: 2.3.1 - rollup: 2.79.1 + rollup: 3.28.1 + serialize-javascript: 6.0.1 + smob: 1.4.1 + terser: 5.18.2 dev: true /@rollup/pluginutils@4.2.1: @@ -6124,6 +6344,21 @@ packages: picomatch: 2.3.1 dev: true + /@rollup/pluginutils@5.0.5(rollup@3.28.1): + resolution: {integrity: sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.1 + estree-walker: 2.0.2 + picomatch: 2.3.1 + rollup: 3.28.1 + dev: true + /@rushstack/eslint-patch@1.2.0: resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} dev: true @@ -6162,12 +6397,6 @@ packages: engines: {node: '>=10'} dev: true - /@sinonjs/commons@1.8.6: - resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==} - dependencies: - type-detect: 4.0.8 - dev: true - /@sinonjs/commons@2.0.0: resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} dependencies: @@ -6180,12 +6409,6 @@ packages: '@sinonjs/commons': 2.0.0 dev: true - /@sinonjs/fake-timers@8.1.0: - resolution: {integrity: sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==} - dependencies: - '@sinonjs/commons': 1.8.6 - dev: true - /@sitespeed.io/tracium@0.3.3: resolution: {integrity: sha512-dNZafjM93Y+F+sfwTO5gTpsGXlnc/0Q+c2+62ViqP3gkMWvHEMSKkaEHgVJLcLg3i/g19GSIPziiKpgyne07Bw==} engines: {node: '>=8'} @@ -6205,14 +6428,14 @@ packages: size-limit: 8.2.6 dev: true - /@size-limit/preset-big-lib@8.2.6(size-limit@8.2.6): + /@size-limit/preset-big-lib@8.2.6(esbuild@0.18.20)(size-limit@8.2.6): resolution: {integrity: sha512-63a+yos0QNMVCfx1OWnxBrdQVTlBVGzW5fDXwpWq/hKfP3B89XXHYGeL2Z2f8IXSVeGkAHXnDcTZyIPRaXffVg==} peerDependencies: size-limit: 8.2.6 dependencies: '@size-limit/file': 8.2.6(size-limit@8.2.6) '@size-limit/time': 8.2.6(size-limit@8.2.6) - '@size-limit/webpack': 8.2.6(size-limit@8.2.6) + '@size-limit/webpack': 8.2.6(esbuild@0.18.20)(size-limit@8.2.6) size-limit: 8.2.6 transitivePeerDependencies: - '@swc/core' @@ -6241,7 +6464,7 @@ packages: - utf-8-validate dev: true - /@size-limit/webpack@8.2.6(size-limit@8.2.6): + /@size-limit/webpack@8.2.6(esbuild@0.18.20)(size-limit@8.2.6): resolution: {integrity: sha512-y2sB66m5sJxIjZ8SEAzpWbiw3/+bnQHDHfk9cSbV5ChKklq02AlYg8BS5KxGWmMpdyUo4TzpjSCP9oEudY+hxQ==} engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0} peerDependencies: @@ -6249,7 +6472,7 @@ packages: dependencies: nanoid: 3.3.6 size-limit: 8.2.6 - webpack: 5.88.1 + webpack: 5.88.1(esbuild@0.18.20) transitivePeerDependencies: - '@swc/core' - esbuild @@ -6338,22 +6561,6 @@ packages: engines: {node: '>= 10'} dev: true - /@tsconfig/node10@1.0.9: - resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} - dev: true - - /@tsconfig/node12@1.0.11: - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - dev: true - - /@tsconfig/node14@1.0.3: - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - dev: true - - /@tsconfig/node16@1.0.4: - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - dev: true - /@tufjs/canonical-json@1.0.0: resolution: {integrity: sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -6414,7 +6621,7 @@ packages: dependencies: '@types/http-cache-semantics': 4.0.1 '@types/keyv': 3.1.4 - '@types/node': 17.0.23 + '@types/node': 20.1.2 '@types/responselike': 1.0.0 dev: true @@ -6461,10 +6668,6 @@ packages: dependencies: '@types/estree': 1.0.1 - /@types/estree@0.0.39: - resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} - dev: true - /@types/estree@1.0.1: resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} @@ -6472,12 +6675,12 @@ packages: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 17.0.23 + '@types/node': 20.1.2 /@types/graceful-fs@4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: - '@types/node': 17.0.23 + '@types/node': 20.1.2 dev: true /@types/hast@2.3.4: @@ -6512,13 +6715,6 @@ packages: dependencies: '@types/istanbul-lib-report': 3.0.0 - /@types/jest@27.5.2: - resolution: {integrity: sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==} - dependencies: - jest-matcher-utils: 27.5.1 - pretty-format: 27.5.1 - dev: true - /@types/jest@29.5.1: resolution: {integrity: sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ==} dependencies: @@ -6533,7 +6729,7 @@ packages: /@types/jsdom@20.0.1: resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} dependencies: - '@types/node': 17.0.23 + '@types/node': 20.1.2 '@types/tough-cookie': 4.0.2 parse5: 7.1.2 dev: true @@ -6556,7 +6752,7 @@ packages: /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 17.0.23 + '@types/node': 20.1.2 dev: true /@types/lodash@4.14.176: @@ -6602,10 +6798,10 @@ packages: /@types/node@17.0.23: resolution: {integrity: sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==} + dev: true /@types/node@20.1.2: resolution: {integrity: sha512-CTO/wa8x+rZU626cL2BlbCDzydgnFNgc19h4YvizpTO88MFQxab8wqisxaofQJ/9bLGugRdWIuX/TbIs6VVF6g==} - dev: true /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} @@ -6626,29 +6822,33 @@ packages: resolution: {integrity: sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==} dev: false + /@types/react-dom@18.2.14: + resolution: {integrity: sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==} + dependencies: + '@types/react': 18.2.34 + dev: true + /@types/react-dom@18.2.2: resolution: {integrity: sha512-IGuuCsLmAH0f3KksOZp/vkpUtO2YrIwob4YxvoFQR2XvkLL7tf7mLYcXiyG47KgTKngI4+7lNm4dM4eBTbG1Bw==} dependencies: - '@types/react': 18.2.5 + '@types/react': 18.2.34 dev: true - /@types/react@18.2.5: - resolution: {integrity: sha512-RuoMedzJ5AOh23Dvws13LU9jpZHIc/k90AgmK7CecAYeWmSr3553L4u5rk4sWAPBuQosfT7HmTfG4Rg5o4nGEA==} + /@types/react@18.2.34: + resolution: {integrity: sha512-U6eW/alrRk37FU/MS2RYMjx0Va2JGIVXELTODaTIYgvWGCV4Y4TfTUzG8DdmpDNIT0Xpj/R7GfyHOJJrDttcvg==} dependencies: '@types/prop-types': 15.7.5 '@types/scheduler': 0.16.3 csstype: 3.1.2 - /@types/resolve@1.17.1: - resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} - dependencies: - '@types/node': 17.0.23 + /@types/resolve@1.20.2: + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} dev: true /@types/responselike@1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: - '@types/node': 17.0.23 + '@types/node': 20.1.2 dev: true /@types/scheduler@0.16.3: @@ -6690,7 +6890,7 @@ packages: /@types/webpack-sources@3.2.0: resolution: {integrity: sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==} dependencies: - '@types/node': 17.0.23 + '@types/node': 20.1.2 '@types/source-list-map': 0.1.2 source-map: 0.7.4 dev: false @@ -6698,7 +6898,7 @@ packages: /@types/webpack@4.41.33: resolution: {integrity: sha512-PPajH64Ft2vWevkerISMtnZ8rTs4YmRbs+23c402J0INmxDKCrhZNvwZYtzx96gY2wAtXdrK1BS2fiC8MlLr3g==} dependencies: - '@types/node': 17.0.23 + '@types/node': 20.1.2 '@types/tapable': 1.0.8 '@types/uglify-js': 3.17.1 '@types/webpack-sources': 3.2.0 @@ -6719,6 +6919,7 @@ packages: resolution: {integrity: sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==} dependencies: '@types/yargs-parser': 21.0.0 + dev: false /@types/yargs@17.0.24: resolution: {integrity: sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==} @@ -6729,39 +6930,11 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 17.0.23 + '@types/node': 20.1.2 dev: true optional: true - /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.59.2)(eslint@8.46.0)(typescript@4.9.5): - resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/parser': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@eslint-community/regexpp': 4.7.0 - '@typescript-eslint/parser': 5.59.2(eslint@8.46.0)(typescript@4.9.5) - '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/type-utils': 5.62.0(eslint@8.46.0)(typescript@4.9.5) - '@typescript-eslint/utils': 5.62.0(eslint@8.46.0)(typescript@4.9.5) - debug: 4.3.4(supports-color@6.1.0) - eslint: 8.46.0 - graphemer: 1.4.0 - ignore: 5.2.4 - natural-compare-lite: 1.4.0 - semver: 7.5.4 - tsutils: 3.21.0(typescript@4.9.5) - typescript: 4.9.5 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/eslint-plugin@6.4.1(@typescript-eslint/parser@6.4.1)(eslint@8.46.0)(typescript@5.0.4): + /@typescript-eslint/eslint-plugin@6.4.1(@typescript-eslint/parser@6.4.1)(eslint@8.46.0)(typescript@5.2.2): resolution: {integrity: sha512-3F5PtBzUW0dYlq77Lcqo13fv+58KDwUib3BddilE8ajPJT+faGgxmI9Sw+I8ZS22BYwoir9ZhNXcLi+S+I2bkw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -6773,10 +6946,10 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.7.0 - '@typescript-eslint/parser': 6.4.1(eslint@8.46.0)(typescript@5.0.4) + '@typescript-eslint/parser': 6.4.1(eslint@8.46.0)(typescript@5.2.2) '@typescript-eslint/scope-manager': 6.4.1 - '@typescript-eslint/type-utils': 6.4.1(eslint@8.46.0)(typescript@5.0.4) - '@typescript-eslint/utils': 6.4.1(eslint@8.46.0)(typescript@5.0.4) + '@typescript-eslint/type-utils': 6.4.1(eslint@8.46.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.4.1(eslint@8.46.0)(typescript@5.2.2) '@typescript-eslint/visitor-keys': 6.4.1 debug: 4.3.4(supports-color@6.1.0) eslint: 8.46.0 @@ -6784,33 +6957,13 @@ packages: ignore: 5.2.4 natural-compare: 1.4.0 semver: 7.5.4 - ts-api-utils: 1.0.2(typescript@5.0.4) - typescript: 5.0.4 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/parser@5.59.2(eslint@8.46.0)(typescript@4.9.5): - resolution: {integrity: sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/scope-manager': 5.59.2 - '@typescript-eslint/types': 5.59.2 - '@typescript-eslint/typescript-estree': 5.59.2(typescript@4.9.5) - debug: 4.3.4(supports-color@6.1.0) - eslint: 8.46.0 - typescript: 4.9.5 + ts-api-utils: 1.0.2(typescript@5.2.2) + typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@5.59.2(eslint@8.46.0)(typescript@5.0.4): + /@typescript-eslint/parser@5.59.2(eslint@8.46.0)(typescript@5.2.2): resolution: {integrity: sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -6822,15 +6975,15 @@ packages: dependencies: '@typescript-eslint/scope-manager': 5.59.2 '@typescript-eslint/types': 5.59.2 - '@typescript-eslint/typescript-estree': 5.59.2(typescript@5.0.4) + '@typescript-eslint/typescript-estree': 5.59.2(typescript@5.2.2) debug: 4.3.4(supports-color@6.1.0) eslint: 8.46.0 - typescript: 5.0.4 + typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@6.4.1(eslint@8.46.0)(typescript@5.0.4): + /@typescript-eslint/parser@6.4.1(eslint@8.46.0)(typescript@5.2.2): resolution: {integrity: sha512-610G6KHymg9V7EqOaNBMtD1GgpAmGROsmfHJPXNLCU9bfIuLrkdOygltK784F6Crboyd5tBFayPB7Sf0McrQwg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -6842,11 +6995,11 @@ packages: dependencies: '@typescript-eslint/scope-manager': 6.4.1 '@typescript-eslint/types': 6.4.1 - '@typescript-eslint/typescript-estree': 6.4.1(typescript@5.0.4) + '@typescript-eslint/typescript-estree': 6.4.1(typescript@5.2.2) '@typescript-eslint/visitor-keys': 6.4.1 debug: 4.3.4(supports-color@6.1.0) eslint: 8.46.0 - typescript: 5.0.4 + typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true @@ -6859,14 +7012,6 @@ packages: '@typescript-eslint/visitor-keys': 5.59.2 dev: true - /@typescript-eslint/scope-manager@5.62.0: - resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/visitor-keys': 5.62.0 - dev: true - /@typescript-eslint/scope-manager@6.4.1: resolution: {integrity: sha512-p/OavqOQfm4/Hdrr7kvacOSFjwQ2rrDVJRPxt/o0TOWdFnjJptnjnZ+sYDR7fi4OimvIuKp+2LCkc+rt9fIW+A==} engines: {node: ^16.0.0 || >=18.0.0} @@ -6875,27 +7020,7 @@ packages: '@typescript-eslint/visitor-keys': 6.4.1 dev: true - /@typescript-eslint/type-utils@5.62.0(eslint@8.46.0)(typescript@4.9.5): - resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '*' - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) - '@typescript-eslint/utils': 5.62.0(eslint@8.46.0)(typescript@4.9.5) - debug: 4.3.4(supports-color@6.1.0) - eslint: 8.46.0 - tsutils: 3.21.0(typescript@4.9.5) - typescript: 4.9.5 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/type-utils@6.4.1(eslint@8.46.0)(typescript@5.0.4): + /@typescript-eslint/type-utils@6.4.1(eslint@8.46.0)(typescript@5.2.2): resolution: {integrity: sha512-7ON8M8NXh73SGZ5XvIqWHjgX2f+vvaOarNliGhjrJnv1vdjG0LVIz+ToYfPirOoBi56jxAKLfsLm40+RvxVVXA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -6905,12 +7030,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.4.1(typescript@5.0.4) - '@typescript-eslint/utils': 6.4.1(eslint@8.46.0)(typescript@5.0.4) + '@typescript-eslint/typescript-estree': 6.4.1(typescript@5.2.2) + '@typescript-eslint/utils': 6.4.1(eslint@8.46.0)(typescript@5.2.2) debug: 4.3.4(supports-color@6.1.0) eslint: 8.46.0 - ts-api-utils: 1.0.2(typescript@5.0.4) - typescript: 5.0.4 + ts-api-utils: 1.0.2(typescript@5.2.2) + typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true @@ -6920,38 +7045,12 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/types@5.62.0: - resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - /@typescript-eslint/types@6.4.1: resolution: {integrity: sha512-zAAopbNuYu++ijY1GV2ylCsQsi3B8QvfPHVqhGdDcbx/NK5lkqMnCGU53amAjccSpk+LfeONxwzUhDzArSfZJg==} engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@5.59.2(typescript@4.9.5): - resolution: {integrity: sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 5.59.2 - '@typescript-eslint/visitor-keys': 5.59.2 - debug: 4.3.4(supports-color@6.1.0) - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.5.4 - tsutils: 3.21.0(typescript@4.9.5) - typescript: 4.9.5 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/typescript-estree@5.59.2(typescript@5.0.4): + /@typescript-eslint/typescript-estree@5.59.2(typescript@5.2.2): resolution: {integrity: sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -6966,34 +7065,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 - tsutils: 3.21.0(typescript@5.0.4) - typescript: 5.0.4 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/typescript-estree@5.62.0(typescript@4.9.5): - resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.3.4(supports-color@6.1.0) - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.5.4 - tsutils: 3.21.0(typescript@4.9.5) - typescript: 4.9.5 + tsutils: 3.21.0(typescript@5.2.2) + typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/typescript-estree@6.4.1(typescript@5.0.4): + /@typescript-eslint/typescript-estree@6.4.1(typescript@5.2.2): resolution: {integrity: sha512-xF6Y7SatVE/OyV93h1xGgfOkHr2iXuo8ip0gbfzaKeGGuKiAnzS+HtVhSPx8Www243bwlW8IF7X0/B62SzFftg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -7008,33 +7086,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 - ts-api-utils: 1.0.2(typescript@5.0.4) - typescript: 5.0.4 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/utils@5.59.2(eslint@8.46.0)(typescript@4.9.5): - resolution: {integrity: sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0) - '@types/json-schema': 7.0.12 - '@types/semver': 7.5.0 - '@typescript-eslint/scope-manager': 5.59.2 - '@typescript-eslint/types': 5.59.2 - '@typescript-eslint/typescript-estree': 5.59.2(typescript@4.9.5) - eslint: 8.46.0 - eslint-scope: 5.1.1 - semver: 7.5.4 + ts-api-utils: 1.0.2(typescript@5.2.2) + typescript: 5.2.2 transitivePeerDependencies: - supports-color - - typescript dev: true - /@typescript-eslint/utils@5.59.2(eslint@8.46.0)(typescript@5.0.4): + /@typescript-eslint/utils@5.59.2(eslint@8.46.0)(typescript@5.2.2): resolution: {integrity: sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -7045,27 +7103,7 @@ packages: '@types/semver': 7.5.0 '@typescript-eslint/scope-manager': 5.59.2 '@typescript-eslint/types': 5.59.2 - '@typescript-eslint/typescript-estree': 5.59.2(typescript@5.0.4) - eslint: 8.46.0 - eslint-scope: 5.1.1 - semver: 7.5.4 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /@typescript-eslint/utils@5.62.0(eslint@8.46.0)(typescript@4.9.5): - resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0) - '@types/json-schema': 7.0.12 - '@types/semver': 7.5.0 - '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) + '@typescript-eslint/typescript-estree': 5.59.2(typescript@5.2.2) eslint: 8.46.0 eslint-scope: 5.1.1 semver: 7.5.4 @@ -7074,7 +7112,7 @@ packages: - typescript dev: true - /@typescript-eslint/utils@6.4.1(eslint@8.46.0)(typescript@5.0.4): + /@typescript-eslint/utils@6.4.1(eslint@8.46.0)(typescript@5.2.2): resolution: {integrity: sha512-F/6r2RieNeorU0zhqZNv89s9bDZSovv3bZQpUNOmmQK1L80/cV4KEu95YUJWi75u5PhboFoKUJBnZ4FQcoqhDw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -7085,7 +7123,7 @@ packages: '@types/semver': 7.5.0 '@typescript-eslint/scope-manager': 6.4.1 '@typescript-eslint/types': 6.4.1 - '@typescript-eslint/typescript-estree': 6.4.1(typescript@5.0.4) + '@typescript-eslint/typescript-estree': 6.4.1(typescript@5.2.2) eslint: 8.46.0 semver: 7.5.4 transitivePeerDependencies: @@ -7101,14 +7139,6 @@ packages: eslint-visitor-keys: 3.4.3 dev: true - /@typescript-eslint/visitor-keys@5.62.0: - resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.62.0 - eslint-visitor-keys: 3.4.3 - dev: true - /@typescript-eslint/visitor-keys@6.4.1: resolution: {integrity: sha512-y/TyRJsbZPkJIZQXrHfdnxVnxyKegnpEvnRGNam7s3TRR2ykGefEWOhaef00/UUN3IZxizS7BTO3svd3lCOJRQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -7179,7 +7209,7 @@ packages: lodash: 4.17.21 mlly: 1.2.0 outdent: 0.8.0 - vite: 4.3.4(@types/node@17.0.23) + vite: 4.3.4(@types/node@20.1.2) vite-node: 0.28.5 transitivePeerDependencies: - '@types/node' @@ -7487,14 +7517,6 @@ packages: /@xtuc/long@4.2.2: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - /@yarn-tool/resolve-package@1.0.47: - resolution: {integrity: sha512-Zaw58gQxjQceJqhqybJi1oUDaORT8i2GTgwICPs8v/X/Pkx35FXQba69ldHVg5pQZ6YLKpROXgyHvBaCJOFXiA==} - dependencies: - pkg-dir: 5.0.0 - tslib: 2.5.0 - upath2: 3.1.19 - dev: true - /@yarnpkg/lockfile@1.1.0: resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} dev: true @@ -7561,13 +7583,6 @@ packages: mime-types: 2.1.35 negotiator: 0.6.3 - /acorn-globals@6.0.0: - resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} - dependencies: - acorn: 7.4.1 - acorn-walk: 7.2.0 - dev: true - /acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} dependencies: @@ -7590,11 +7605,6 @@ packages: dependencies: acorn: 8.10.0 - /acorn-walk@7.2.0: - resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} - engines: {node: '>=0.4.0'} - dev: true - /acorn-walk@8.2.0: resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} engines: {node: '>=0.4.0'} @@ -7606,12 +7616,6 @@ packages: hasBin: true dev: false - /acorn@7.4.1: - resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true - /acorn@8.10.0: resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} engines: {node: '>=0.4.0'} @@ -7706,6 +7710,7 @@ packages: /ansi-escapes@3.2.0: resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} engines: {node: '>=4'} + dev: false /ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} @@ -7732,15 +7737,9 @@ packages: engines: {node: '>=0.10.0'} dev: false - /ansi-regex@3.0.1: - resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} - engines: {node: '>=4'} - dev: true - /ansi-regex@4.1.1: resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} engines: {node: '>=6'} - dev: false /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} @@ -7838,6 +7837,7 @@ packages: /arg@4.1.0: resolution: {integrity: sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==} + dev: false /arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -8076,10 +8076,6 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - /asyncro@3.0.0: - resolution: {integrity: sha512-nEnWYfrBmA3taTiuiOoZYmgJ/CNrSoQLeLs29SeLcPu60yaw/mHDBHV0iOZ051fTvsTHxpCY+gXibqT9wbQYfg==} - dev: true - /at-least-node@1.0.0: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} @@ -8088,6 +8084,7 @@ packages: resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} engines: {node: '>= 4.5.0'} hasBin: true + dev: false /autoprefixer@10.4.0(postcss@8.4.23): resolution: {integrity: sha512-7FdJ1ONtwzV1G43GDD0kpVMn/qbiNqyOPMFTX5nRffI+7vgWoFEc6DcXOxHJxrWNDXrZh18eDsZjvZGUljSRGA==} @@ -8146,6 +8143,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: @@ -8154,25 +8155,6 @@ packages: '@babel/core': 7.22.5 dev: false - /babel-jest@27.5.1(@babel/core@7.22.5): - resolution: {integrity: sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - peerDependencies: - '@babel/core': ^7.8.0 - dependencies: - '@babel/core': 7.22.5 - '@jest/transform': 27.5.1 - '@jest/types': 27.5.1 - '@types/babel__core': 7.20.0 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 27.5.1(@babel/core@7.22.5) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - dev: true - /babel-jest@29.5.0(@babel/core@7.22.5): resolution: {integrity: sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -8207,22 +8189,6 @@ packages: webpack: 4.43.0 dev: false - /babel-plugin-annotate-pure-calls@0.4.0(@babel/core@7.22.5): - resolution: {integrity: sha512-oi4M/PWUJOU9ZyRGoPTfPMqdyMp06jbJAomd3RcyYuzUtBOddv98BqLm96Lucpi2QFoQHkdGQt0ACvw7VzVEQA==} - peerDependencies: - '@babel/core': ^6.0.0-0 || 7.x - dependencies: - '@babel/core': 7.22.5 - dev: true - - /babel-plugin-dev-expression@0.2.3(@babel/core@7.22.5): - resolution: {integrity: sha512-rP5LK9QQTzCW61nVVzw88En1oK8t8gTsIeC6E61oelxNsU842yMjF0G1MxhvUpCkxCEIj7sE8/e5ieTheT//uw==} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.22.5 - dev: true - /babel-plugin-istanbul@6.1.1: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} engines: {node: '>=8'} @@ -8236,16 +8202,6 @@ packages: - supports-color dev: true - /babel-plugin-jest-hoist@27.5.1: - resolution: {integrity: sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@babel/template': 7.22.5 - '@babel/types': 7.22.11 - '@types/babel__core': 7.20.0 - '@types/babel__traverse': 7.18.5 - dev: true - /babel-plugin-jest-hoist@29.5.0: resolution: {integrity: sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -8256,15 +8212,6 @@ packages: '@types/babel__traverse': 7.18.5 dev: true - /babel-plugin-macros@3.1.0: - resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} - engines: {node: '>=10', npm: '>=6'} - dependencies: - '@babel/runtime': 7.21.5 - cosmiconfig: 7.0.0 - resolve: 1.22.2 - dev: true - /babel-plugin-module-resolver@4.1.0: resolution: {integrity: sha512-MlX10UDheRr3lb3P0WcaIdtCSRlxdQsB1sBqL7W0raF070bGl1HQQq5K3T2vf2XAYie+ww+5AKC/WrkjRO2knA==} engines: {node: '>= 8.0.0'} @@ -8376,17 +8323,6 @@ packages: - supports-color dev: true - /babel-plugin-polyfill-regenerator@0.3.1(@babel/core@7.22.5): - resolution: {integrity: sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.22.5) - transitivePeerDependencies: - - supports-color - dev: true - /babel-plugin-polyfill-regenerator@0.4.1(@babel/core@7.21.8): resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==} peerDependencies: @@ -8439,10 +8375,6 @@ packages: resolution: {integrity: sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==} dev: false - /babel-plugin-transform-rename-import@2.3.0: - resolution: {integrity: sha512-dPgJoT57XC0PqSnLgl2FwNvxFrWlspatX2dkk7yjKQj5HHGw071vAcOf+hqW8ClqcBDMvEbm6mevn5yHAD8mlQ==} - dev: true - /babel-preset-current-node-syntax@1.0.1(@babel/core@7.22.5): resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} peerDependencies: @@ -8548,17 +8480,6 @@ packages: babel-plugin-syntax-trailing-function-commas: 7.0.0-beta.0 dev: false - /babel-preset-jest@27.5.1(@babel/core@7.22.5): - resolution: {integrity: sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.22.5 - babel-plugin-jest-hoist: 27.5.1 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.5) - dev: true - /babel-preset-jest@29.5.0(@babel/core@7.22.5): resolution: {integrity: sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -8786,11 +8707,7 @@ packages: /brorand@1.1.0: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} - dev: false - - /browser-process-hrtime@1.0.0: - resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} - dev: true + dev: false /browserify-aes@1.2.0: resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} @@ -8885,13 +8802,6 @@ packages: node-releases: 2.0.12 update-browserslist-db: 1.0.11(browserslist@4.21.9) - /bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - dependencies: - fast-json-stable-stringify: 2.1.0 - dev: true - /bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} dependencies: @@ -8973,6 +8883,16 @@ packages: run-applescript: 5.0.0 dev: true + /bundle-require@4.0.2(esbuild@0.18.20): + resolution: {integrity: sha512-jwzPOChofl67PSTW2SGubV9HBQAhhR2i6nskiOThauo9dzwDUgOWQScFVaJkjEfYX+UXiD+LEx8EblQMc2wIag==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.17' + dependencies: + esbuild: 0.18.20 + load-tsconfig: 0.2.5 + dev: true + /busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -9276,6 +9196,17 @@ packages: resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} dev: true + /chokidar-cli@3.0.0: + resolution: {integrity: sha512-xVW+Qeh7z15uZRxHOkP93Ux8A0xbPzwK4GaqD8dQOYc34TlkqUhVSS59fK36DOp5WdJlrRzlYSy02Ht99FjZqQ==} + engines: {node: '>= 8.10.0'} + hasBin: true + dependencies: + chokidar: 3.5.3 + lodash.debounce: 4.0.8 + lodash.throttle: 4.1.1 + yargs: 13.3.2 + dev: true + /chokidar@2.1.8(supports-color@6.1.0): resolution: {integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==} deprecated: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies @@ -9384,6 +9315,7 @@ packages: engines: {node: '>=4'} dependencies: restore-cursor: 2.0.0 + dev: false /cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} @@ -9391,11 +9323,6 @@ packages: dependencies: restore-cursor: 3.1.0 - /cli-spinners@1.3.1: - resolution: {integrity: sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg==} - engines: {node: '>=4'} - dev: true - /cli-spinners@2.6.1: resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} engines: {node: '>=6'} @@ -9427,7 +9354,6 @@ packages: string-width: 3.1.0 strip-ansi: 5.2.0 wrap-ansi: 5.1.0 - dev: false /cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} @@ -9547,7 +9473,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==} @@ -9561,6 +9486,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 @@ -9973,10 +9906,6 @@ packages: object-assign: 4.1.1 dev: false - /create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - dev: true - /cross-fetch@3.1.5: resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} dependencies: @@ -10218,10 +10147,6 @@ packages: resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} dev: true - /cssom@0.4.4: - resolution: {integrity: sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==} - dev: true - /cssom@0.5.0: resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} dev: true @@ -10538,15 +10463,6 @@ packages: resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} engines: {node: '>= 6'} - /data-urls@2.0.0: - resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==} - engines: {node: '>=10'} - dependencies: - abab: 2.0.6 - whatwg-mimetype: 2.3.0 - whatwg-url: 8.7.0 - dev: true - /data-urls@3.0.2: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} engines: {node: '>=12'} @@ -10643,6 +10559,7 @@ packages: /decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} + dev: false /decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} @@ -10699,7 +10616,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==} @@ -10816,20 +10732,6 @@ packages: rimraf: 2.7.1 dev: false - /del@5.1.0: - resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} - engines: {node: '>=8'} - dependencies: - globby: 10.0.2 - graceful-fs: 4.2.11 - is-glob: 4.0.3 - is-path-cwd: 2.2.0 - is-path-inside: 3.0.3 - p-map: 3.0.0 - rimraf: 3.0.2 - slash: 3.0.0 - dev: true - /del@6.1.1: resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} engines: {node: '>=10'} @@ -10899,6 +10801,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'} @@ -10932,21 +10839,11 @@ packages: /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - /diff-sequences@27.5.1: - resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dev: true - /diff-sequences@29.4.3: resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} - engines: {node: '>=0.3.1'} - dev: true - /diff@5.1.0: resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} engines: {node: '>=0.3.1'} @@ -11037,13 +10934,6 @@ packages: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} dev: false - /domexception@2.0.1: - resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==} - engines: {node: '>=8'} - dependencies: - webidl-conversions: 5.0.0 - dev: true - /domexception@4.0.0: resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} engines: {node: '>=12'} @@ -11114,93 +11004,6 @@ packages: engines: {node: '>=12'} dev: true - /dts-cli@1.4.0(@babel/plugin-syntax-flow@7.21.4)(@babel/plugin-transform-react-jsx@7.22.5)(@types/node@17.0.23): - resolution: {integrity: sha512-bcjolZYKh51WPurWUayO85XaqccE26yWiAd5+JCZU8YFWAJgcp+UGFke3OwmJvo7WKX3e48FuvzFezUkd+eFSg==} - engines: {node: '>=12.0.0'} - hasBin: true - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-module-imports': 7.22.5 - '@babel/parser': 7.22.5 - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.22.5) - '@babel/preset-env': 7.22.14(@babel/core@7.22.5) - '@babel/traverse': 7.22.5 - '@rollup/plugin-babel': 5.3.1(@babel/core@7.22.5)(rollup@2.79.1) - '@rollup/plugin-commonjs': 21.1.0(rollup@2.79.1) - '@rollup/plugin-json': 4.1.0(rollup@2.79.1) - '@rollup/plugin-node-resolve': 13.3.0(rollup@2.79.1) - '@rollup/plugin-replace': 3.1.0(rollup@2.79.1) - '@types/jest': 27.5.2 - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.59.2)(eslint@8.46.0)(typescript@4.9.5) - '@typescript-eslint/parser': 5.59.2(eslint@8.46.0)(typescript@4.9.5) - ansi-escapes: 4.3.2 - asyncro: 3.0.0 - babel-plugin-annotate-pure-calls: 0.4.0(@babel/core@7.22.5) - babel-plugin-dev-expression: 0.2.3(@babel/core@7.22.5) - babel-plugin-macros: 3.1.0 - babel-plugin-polyfill-regenerator: 0.3.1(@babel/core@7.22.5) - babel-plugin-transform-rename-import: 2.3.0 - camelcase: 6.3.0 - chalk: 4.1.2 - confusing-browser-globals: 1.0.11 - enquirer: 2.3.6 - eslint: 8.46.0 - eslint-config-prettier: 8.10.0(eslint@8.46.0) - eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.21.4)(@babel/plugin-transform-react-jsx@7.22.5)(eslint@8.46.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.2)(eslint-import-resolver-typescript@3.5.5)(eslint@8.46.0) - eslint-plugin-jest: 26.9.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.46.0)(jest@27.5.1)(typescript@4.9.5) - eslint-plugin-jsx-a11y: 6.7.1(eslint@8.46.0) - eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.10.0)(eslint@8.46.0)(prettier@2.7.1) - eslint-plugin-react: 7.33.2(eslint@8.46.0) - eslint-plugin-react-hooks: 4.6.0(eslint@8.46.0) - eslint-plugin-testing-library: 5.11.1(eslint@8.46.0)(typescript@4.9.5) - execa: 4.1.0 - figlet: 1.6.0 - fs-extra: 10.1.0 - jest: 27.5.1(ts-node@10.9.1) - jest-watch-typeahead: 0.6.5(jest@27.5.1) - jpjs: 1.2.1 - lodash.merge: 4.6.2 - ora: 5.4.1 - pascal-case: 3.1.2 - postcss: 8.4.24 - prettier: 2.7.1 - progress-estimator: 0.3.1 - regenerator-runtime: 0.13.11 - rollup: 2.79.1 - rollup-plugin-delete: 2.0.0 - rollup-plugin-dts: 4.2.3(rollup@2.79.1)(typescript@4.9.5) - rollup-plugin-sourcemaps: 0.6.3(@types/node@17.0.23)(rollup@2.79.1) - rollup-plugin-terser: 7.0.2(rollup@2.79.1) - rollup-plugin-typescript2: 0.31.2(rollup@2.79.1)(typescript@4.9.5) - sade: 1.8.1 - semver: 7.5.4 - shelljs: 0.8.5 - sort-package-json: 1.57.0 - tiny-glob: 0.2.9 - ts-jest: 27.1.5(@babel/core@7.22.5)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) - ts-node: 10.9.1(@types/node@17.0.23)(typescript@4.9.5) - tslib: 2.5.0 - type-fest: 2.19.0 - typescript: 4.9.5 - transitivePeerDependencies: - - '@babel/plugin-syntax-flow' - - '@babel/plugin-transform-react-jsx' - - '@swc/core' - - '@swc/wasm' - - '@types/babel__core' - - '@types/node' - - babel-jest - - bufferutil - - canvas - - esbuild - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - node-notifier - - supports-color - - utf-8-validate - dev: true - /duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -11251,14 +11054,8 @@ packages: engines: {node: '>=12'} dev: true - /emittery@0.8.1: - resolution: {integrity: sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==} - engines: {node: '>=10'} - dev: true - /emoji-regex@7.0.3: resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==} - dev: false /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -11574,6 +11371,36 @@ packages: '@esbuild/win32-x64': 0.17.6 dev: true + /esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + dev: true + /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -11625,25 +11452,25 @@ packages: source-map: 0.6.1 dev: true - /eslint-config-molindo@7.0.0(eslint@8.46.0)(jest@27.4.5)(tailwindcss@3.3.3)(typescript@5.0.4): + /eslint-config-molindo@7.0.0(eslint@8.46.0)(jest@29.5.0)(tailwindcss@3.3.2)(typescript@5.2.2): resolution: {integrity: sha512-jsy+1xutRhBYOD8EyyOlQRPK9n23yxixfXWEl6ttzTNhV/B8893e09sZDGRc+VK7z4yGW6Pe6cQM9oZkJuEu3Q==} engines: {node: '>=10'} peerDependencies: eslint: ^8.0.0 dependencies: - '@typescript-eslint/eslint-plugin': 6.4.1(@typescript-eslint/parser@6.4.1)(eslint@8.46.0)(typescript@5.0.4) - '@typescript-eslint/parser': 6.4.1(eslint@8.46.0)(typescript@5.0.4) + '@typescript-eslint/eslint-plugin': 6.4.1(@typescript-eslint/parser@6.4.1)(eslint@8.46.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.4.1(eslint@8.46.0)(typescript@5.2.2) confusing-browser-globals: 1.0.11 eslint: 8.46.0 eslint-plugin-css-modules: 2.11.0(eslint@8.46.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.2)(eslint-import-resolver-typescript@3.5.5)(eslint@8.46.0) - eslint-plugin-jest: 27.2.3(@typescript-eslint/eslint-plugin@6.4.1)(eslint@8.46.0)(jest@27.4.5)(typescript@5.0.4) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@6.4.1)(eslint@8.46.0) + eslint-plugin-jest: 27.2.3(@typescript-eslint/eslint-plugin@6.4.1)(eslint@8.46.0)(jest@29.5.0)(typescript@5.2.2) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.46.0) eslint-plugin-prettier: 5.0.0(eslint@8.46.0)(prettier@3.0.2) eslint-plugin-react: 7.33.2(eslint@8.46.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.46.0) eslint-plugin-sort-destructure-keys: 1.5.0(eslint@8.46.0) - eslint-plugin-tailwindcss: 3.13.0(tailwindcss@3.3.3) + eslint-plugin-tailwindcss: 3.13.0(tailwindcss@3.3.2) eslint-plugin-unicorn: 48.0.1(eslint@8.46.0) prettier: 3.0.2 transitivePeerDependencies: @@ -11657,19 +11484,19 @@ packages: - typescript dev: true - /eslint-config-molindo@7.0.0(eslint@8.46.0)(jest@27.5.1)(tailwindcss@3.3.3)(typescript@5.0.4): + /eslint-config-molindo@7.0.0(eslint@8.46.0)(jest@29.5.0)(tailwindcss@3.3.3)(typescript@5.2.2): resolution: {integrity: sha512-jsy+1xutRhBYOD8EyyOlQRPK9n23yxixfXWEl6ttzTNhV/B8893e09sZDGRc+VK7z4yGW6Pe6cQM9oZkJuEu3Q==} engines: {node: '>=10'} peerDependencies: eslint: ^8.0.0 dependencies: - '@typescript-eslint/eslint-plugin': 6.4.1(@typescript-eslint/parser@6.4.1)(eslint@8.46.0)(typescript@5.0.4) - '@typescript-eslint/parser': 6.4.1(eslint@8.46.0)(typescript@5.0.4) + '@typescript-eslint/eslint-plugin': 6.4.1(@typescript-eslint/parser@6.4.1)(eslint@8.46.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.4.1(eslint@8.46.0)(typescript@5.2.2) confusing-browser-globals: 1.0.11 eslint: 8.46.0 eslint-plugin-css-modules: 2.11.0(eslint@8.46.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.2)(eslint-import-resolver-typescript@3.5.5)(eslint@8.46.0) - eslint-plugin-jest: 27.2.3(@typescript-eslint/eslint-plugin@6.4.1)(eslint@8.46.0)(jest@27.5.1)(typescript@5.0.4) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@6.4.1)(eslint@8.46.0) + eslint-plugin-jest: 27.2.3(@typescript-eslint/eslint-plugin@6.4.1)(eslint@8.46.0)(jest@29.5.0)(typescript@5.2.2) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.46.0) eslint-plugin-prettier: 5.0.0(eslint@8.46.0)(prettier@3.0.2) eslint-plugin-react: 7.33.2(eslint@8.46.0) @@ -11689,25 +11516,25 @@ packages: - typescript dev: true - /eslint-config-molindo@7.0.0(eslint@8.46.0)(jest@29.5.0)(tailwindcss@3.3.2)(typescript@5.0.4): - resolution: {integrity: sha512-jsy+1xutRhBYOD8EyyOlQRPK9n23yxixfXWEl6ttzTNhV/B8893e09sZDGRc+VK7z4yGW6Pe6cQM9oZkJuEu3Q==} + /eslint-config-molindo@7.0.0-alpha.7(eslint@8.46.0)(tailwindcss@3.3.3)(typescript@5.2.2): + resolution: {integrity: sha512-aVC9TVo/dhGaICwEVHm1ZIEmxcpPePmarCPFaf+IV7pbE3yqm0j8xABwLyOD9xR31Y6FamROpp9H0R9aez3Mrw==} engines: {node: '>=10'} peerDependencies: eslint: ^8.0.0 dependencies: - '@typescript-eslint/eslint-plugin': 6.4.1(@typescript-eslint/parser@6.4.1)(eslint@8.46.0)(typescript@5.0.4) - '@typescript-eslint/parser': 6.4.1(eslint@8.46.0)(typescript@5.0.4) + '@typescript-eslint/eslint-plugin': 6.4.1(@typescript-eslint/parser@6.4.1)(eslint@8.46.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.4.1(eslint@8.46.0)(typescript@5.2.2) confusing-browser-globals: 1.0.11 eslint: 8.46.0 eslint-plugin-css-modules: 2.11.0(eslint@8.46.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.2)(eslint-import-resolver-typescript@3.5.5)(eslint@8.46.0) - eslint-plugin-jest: 27.2.3(@typescript-eslint/eslint-plugin@6.4.1)(eslint@8.46.0)(jest@29.5.0)(typescript@5.0.4) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@6.4.1)(eslint@8.46.0) + eslint-plugin-jest: 27.2.3(@typescript-eslint/eslint-plugin@6.4.1)(eslint@8.46.0)(jest@29.5.0)(typescript@5.2.2) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.46.0) eslint-plugin-prettier: 5.0.0(eslint@8.46.0)(prettier@3.0.2) eslint-plugin-react: 7.33.2(eslint@8.46.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.46.0) eslint-plugin-sort-destructure-keys: 1.5.0(eslint@8.46.0) - eslint-plugin-tailwindcss: 3.13.0(tailwindcss@3.3.2) + eslint-plugin-tailwindcss: 3.13.0(tailwindcss@3.3.3) eslint-plugin-unicorn: 48.0.1(eslint@8.46.0) prettier: 3.0.2 transitivePeerDependencies: @@ -11721,7 +11548,7 @@ packages: - typescript dev: true - /eslint-config-next@13.4.0(eslint@8.46.0)(typescript@5.0.4): + /eslint-config-next@13.4.0(eslint@8.46.0)(typescript@5.2.2): resolution: {integrity: sha512-FkO3QRyUEKAHM4ie0xAcxo7fQ8gWevuLqgf6/g1Y6zWybqSa4FNeJr4hqqTbP25xIRgUUIPILBlx9RSH4C6+gQ==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 @@ -11732,29 +11559,20 @@ packages: dependencies: '@next/eslint-plugin-next': 13.4.0 '@rushstack/eslint-patch': 1.2.0 - '@typescript-eslint/parser': 5.59.2(eslint@8.46.0)(typescript@5.0.4) + '@typescript-eslint/parser': 5.59.2(eslint@8.46.0)(typescript@5.2.2) eslint: 8.46.0 eslint-import-resolver-node: 0.3.7 eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.2)(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.27.5)(eslint@8.46.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.2)(eslint-import-resolver-typescript@3.5.5)(eslint@8.46.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@6.4.1)(eslint@8.46.0) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.46.0) eslint-plugin-react: 7.32.2(eslint@8.46.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.46.0) - typescript: 5.0.4 + typescript: 5.2.2 transitivePeerDependencies: - eslint-import-resolver-webpack - supports-color dev: true - /eslint-config-prettier@8.10.0(eslint@8.46.0): - resolution: {integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - dependencies: - eslint: 8.46.0 - dev: true - /eslint-import-resolver-node@0.3.7: resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} dependencies: @@ -11776,7 +11594,7 @@ packages: enhanced-resolve: 5.13.0 eslint: 8.46.0 eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.2)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.46.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.2)(eslint-import-resolver-typescript@3.5.5)(eslint@8.46.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@6.4.1)(eslint@8.46.0) get-tsconfig: 4.5.0 globby: 13.1.4 is-core-module: 2.12.0 @@ -11810,7 +11628,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.59.2(eslint@8.46.0)(typescript@5.0.4) + '@typescript-eslint/parser': 5.59.2(eslint@8.46.0)(typescript@5.2.2) debug: 3.2.7(supports-color@6.1.0) eslint: 8.46.0 eslint-import-resolver-node: 0.3.7 @@ -11819,6 +11637,35 @@ packages: - supports-color dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.4.1)(eslint-import-resolver-node@0.3.7)(eslint@8.46.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.4.1(eslint@8.46.0)(typescript@5.2.2) + debug: 3.2.7(supports-color@6.1.0) + eslint: 8.46.0 + eslint-import-resolver-node: 0.3.7 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-plugin-css-modules@2.11.0(eslint@8.46.0): resolution: {integrity: sha512-CLvQvJOMlCywZzaI4HVu7QH/ltgNXvCg7giJGiE+sA9wh5zQ+AqTgftAzrERV22wHe1p688wrU/Zwxt1Ry922w==} engines: {node: '>=4.0.0'} @@ -11830,37 +11677,22 @@ packages: lodash: 4.17.21 dev: true - /eslint-plugin-deprecation@1.4.1(eslint@8.46.0)(typescript@5.0.4): + /eslint-plugin-deprecation@1.4.1(eslint@8.46.0)(typescript@5.2.2): resolution: {integrity: sha512-4vxTghWzxsBukPJVQupi6xlTuDc8Pyi1QlRCrFiLgwLPMJQW3cJCNaehJUKQqQFvuue5m4W27e179Y3Qjzeghg==} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 typescript: ^3.7.5 || ^4.0.0 || ^5.0.0 dependencies: - '@typescript-eslint/utils': 5.59.2(eslint@8.46.0)(typescript@5.0.4) + '@typescript-eslint/utils': 5.59.2(eslint@8.46.0)(typescript@5.2.2) eslint: 8.46.0 tslib: 2.5.0 - tsutils: 3.21.0(typescript@5.0.4) - typescript: 5.0.4 + tsutils: 3.21.0(typescript@5.2.2) + typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.21.4)(@babel/plugin-transform-react-jsx@7.22.5)(eslint@8.46.0): - resolution: {integrity: sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==} - engines: {node: '>=12.0.0'} - peerDependencies: - '@babel/plugin-syntax-flow': ^7.14.5 - '@babel/plugin-transform-react-jsx': ^7.14.9 - eslint: ^8.1.0 - dependencies: - '@babel/plugin-syntax-flow': 7.21.4(@babel/core@7.22.5) - '@babel/plugin-transform-react-jsx': 7.22.5(@babel/core@7.22.5) - eslint: 8.46.0 - lodash: 4.17.21 - string-natural-compare: 3.0.1 - dev: true - - /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.2)(eslint-import-resolver-typescript@3.5.5)(eslint@8.46.0): + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@6.4.1)(eslint@8.46.0): resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} peerDependencies: @@ -11870,7 +11702,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.59.2(eslint@8.46.0)(typescript@5.0.4) + '@typescript-eslint/parser': 6.4.1(eslint@8.46.0)(typescript@5.2.2) array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 @@ -11878,7 +11710,7 @@ packages: doctrine: 2.1.0 eslint: 8.46.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.2)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.46.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.4.1)(eslint-import-resolver-node@0.3.7)(eslint@8.46.0) has: 1.0.3 is-core-module: 2.12.0 is-glob: 4.0.3 @@ -11893,73 +11725,7 @@ packages: - supports-color dev: true - /eslint-plugin-jest@26.9.0(@typescript-eslint/eslint-plugin@5.62.0)(eslint@8.46.0)(jest@27.5.1)(typescript@4.9.5): - resolution: {integrity: sha512-TWJxWGp1J628gxh2KhaH1H1paEdgE2J61BBF1I59c6xWeL5+D1BzMxGDN/nXAfX+aSkR5u80K+XhskK6Gwq9ng==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/eslint-plugin': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - jest: '*' - peerDependenciesMeta: - '@typescript-eslint/eslint-plugin': - optional: true - jest: - optional: true - dependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.59.2)(eslint@8.46.0)(typescript@4.9.5) - '@typescript-eslint/utils': 5.59.2(eslint@8.46.0)(typescript@4.9.5) - eslint: 8.46.0 - jest: 27.5.1(ts-node@10.9.1) - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /eslint-plugin-jest@27.2.3(@typescript-eslint/eslint-plugin@6.4.1)(eslint@8.46.0)(jest@27.4.5)(typescript@5.0.4): - resolution: {integrity: sha512-sRLlSCpICzWuje66Gl9zvdF6mwD5X86I4u55hJyFBsxYOsBCmT5+kSUjf+fkFWVMMgpzNEupjW8WzUqi83hJAQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@typescript-eslint/eslint-plugin': ^5.0.0 || ^6.0.0 - eslint: ^7.0.0 || ^8.0.0 - jest: '*' - peerDependenciesMeta: - '@typescript-eslint/eslint-plugin': - optional: true - jest: - optional: true - dependencies: - '@typescript-eslint/eslint-plugin': 6.4.1(@typescript-eslint/parser@6.4.1)(eslint@8.46.0)(typescript@5.0.4) - '@typescript-eslint/utils': 5.59.2(eslint@8.46.0)(typescript@5.0.4) - eslint: 8.46.0 - jest: 27.4.5 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /eslint-plugin-jest@27.2.3(@typescript-eslint/eslint-plugin@6.4.1)(eslint@8.46.0)(jest@27.5.1)(typescript@5.0.4): - resolution: {integrity: sha512-sRLlSCpICzWuje66Gl9zvdF6mwD5X86I4u55hJyFBsxYOsBCmT5+kSUjf+fkFWVMMgpzNEupjW8WzUqi83hJAQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@typescript-eslint/eslint-plugin': ^5.0.0 || ^6.0.0 - eslint: ^7.0.0 || ^8.0.0 - jest: '*' - peerDependenciesMeta: - '@typescript-eslint/eslint-plugin': - optional: true - jest: - optional: true - dependencies: - '@typescript-eslint/eslint-plugin': 6.4.1(@typescript-eslint/parser@6.4.1)(eslint@8.46.0)(typescript@5.0.4) - '@typescript-eslint/utils': 5.59.2(eslint@8.46.0)(typescript@5.0.4) - eslint: 8.46.0 - jest: 27.5.1(ts-node@10.9.1) - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /eslint-plugin-jest@27.2.3(@typescript-eslint/eslint-plugin@6.4.1)(eslint@8.46.0)(jest@29.5.0)(typescript@5.0.4): + /eslint-plugin-jest@27.2.3(@typescript-eslint/eslint-plugin@6.4.1)(eslint@8.46.0)(jest@29.5.0)(typescript@5.2.2): resolution: {integrity: sha512-sRLlSCpICzWuje66Gl9zvdF6mwD5X86I4u55hJyFBsxYOsBCmT5+kSUjf+fkFWVMMgpzNEupjW8WzUqi83hJAQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -11972,8 +11738,8 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 6.4.1(@typescript-eslint/parser@6.4.1)(eslint@8.46.0)(typescript@5.0.4) - '@typescript-eslint/utils': 5.59.2(eslint@8.46.0)(typescript@5.0.4) + '@typescript-eslint/eslint-plugin': 6.4.1(@typescript-eslint/parser@6.4.1)(eslint@8.46.0)(typescript@5.2.2) + '@typescript-eslint/utils': 5.59.2(eslint@8.46.0)(typescript@5.2.2) eslint: 8.46.0 jest: 29.5.0(@types/node@17.0.23) transitivePeerDependencies: @@ -12006,23 +11772,6 @@ packages: semver: 6.3.1 dev: true - /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.10.0)(eslint@8.46.0)(prettier@2.7.1): - resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} - engines: {node: '>=12.0.0'} - peerDependencies: - eslint: '>=7.28.0' - eslint-config-prettier: '*' - prettier: '>=2.0.0' - peerDependenciesMeta: - eslint-config-prettier: - optional: true - dependencies: - eslint: 8.46.0 - eslint-config-prettier: 8.10.0(eslint@8.46.0) - prettier: 2.7.1 - prettier-linter-helpers: 1.0.0 - dev: true - /eslint-plugin-prettier@5.0.0(eslint@8.46.0)(prettier@3.0.2): resolution: {integrity: sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==} engines: {node: ^14.18.0 || >=16.0.0} @@ -12118,7 +11867,7 @@ packages: tailwindcss: ^3.3.2 dependencies: fast-glob: 3.2.12 - postcss: 8.4.24 + postcss: 8.4.31 tailwindcss: 3.3.2 dev: true @@ -12129,23 +11878,10 @@ packages: tailwindcss: ^3.3.2 dependencies: fast-glob: 3.2.12 - postcss: 8.4.24 + postcss: 8.4.31 tailwindcss: 3.3.3 dev: true - /eslint-plugin-testing-library@5.11.1(eslint@8.46.0)(typescript@4.9.5): - resolution: {integrity: sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} - peerDependencies: - eslint: ^7.5.0 || ^8.0.0 - dependencies: - '@typescript-eslint/utils': 5.59.2(eslint@8.46.0)(typescript@4.9.5) - eslint: 8.46.0 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - /eslint-plugin-unicorn@48.0.1(eslint@8.46.0): resolution: {integrity: sha512-FW+4r20myG/DqFcCSzoumaddKBicIPeFnTrifon2mWIzlfyvzwyqZjqVP7m4Cqr/ZYisS2aiLghkUWaPg6vtCw==} engines: {node: '>=16'} @@ -12340,10 +12076,6 @@ packages: resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} dev: true - /estree-walker@1.0.1: - resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} - dev: true - /estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} dev: true @@ -12418,23 +12150,8 @@ packages: npm-run-path: 2.0.2 p-finally: 1.0.0 signal-exit: 3.0.7 - strip-eof: 1.0.0 - dev: false - - /execa@4.1.0: - resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} - engines: {node: '>=10'} - dependencies: - cross-spawn: 7.0.3 - get-stream: 5.2.0 - human-signals: 1.1.1 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - dev: true + strip-eof: 1.0.0 + dev: false /execa@5.0.0: resolution: {integrity: sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==} @@ -12506,14 +12223,9 @@ packages: - supports-color dev: false - /expect@27.5.1: - resolution: {integrity: sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/types': 27.5.1 - jest-get-type: 27.5.1 - jest-matcher-utils: 27.5.1 - jest-message-util: 27.5.1 + /expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} dev: true /expect@29.5.0: @@ -12772,6 +12484,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'} @@ -12889,12 +12605,6 @@ packages: resolution: {integrity: sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==} dev: false - /figlet@1.6.0: - resolution: {integrity: sha512-31EQGhCEITv6+hi2ORRPyn3bulaV9Fl4xOdR169cBzH/n1UqcxsiSB/noo6SJdD7Kfb1Ljit+IgR1USvF/XbdA==} - engines: {node: '>= 0.4.0'} - hasBin: true - dev: true - /figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -13015,6 +12725,7 @@ packages: commondir: 1.0.1 make-dir: 3.1.0 pkg-dir: 4.2.0 + dev: false /find-chrome-bin@0.1.0: resolution: {integrity: sha512-XoFZwaEn1R3pE6zNG8kH64l2e093hgB9+78eEKPmJK0o1EXEou+25cEWdtu2qq4DBQPDSe90VJAWVI2Sz9pX6Q==} @@ -13033,7 +12744,6 @@ packages: engines: {node: '>=6'} dependencies: locate-path: 3.0.0 - dev: false /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} @@ -13125,7 +12835,7 @@ packages: signal-exit: 4.1.0 dev: true - /fork-ts-checker-webpack-plugin@4.1.6(typescript@5.0.4)(webpack@4.43.0): + /fork-ts-checker-webpack-plugin@4.1.6(typescript@5.2.2)(webpack@4.43.0): resolution: {integrity: sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==} engines: {node: '>=6.11.5', yarn: '>=1.0.0'} peerDependencies: @@ -13145,7 +12855,7 @@ packages: minimatch: 3.1.2 semver: 5.7.1 tapable: 1.1.3 - typescript: 5.0.4 + typescript: 5.2.2 webpack: 4.43.0 worker-rpc: 0.1.1 transitivePeerDependencies: @@ -13159,6 +12869,7 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 + dev: false /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} @@ -13532,6 +13243,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 @@ -13678,10 +13393,6 @@ packages: dependencies: define-properties: 1.2.0 - /globalyzer@0.1.0: - resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} - dev: true - /globby@10.0.0: resolution: {integrity: sha512-3LifW9M4joGZasyYPz2A1U74zbC/45fvpXUvO/9KbSa+VV0aGZarWkfdgKyR9sExNP0t0x0ss/UMJpNpcaTspw==} engines: {node: '>=8'} @@ -13696,20 +13407,6 @@ packages: slash: 3.0.0 dev: true - /globby@10.0.2: - resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} - engines: {node: '>=8'} - dependencies: - '@types/glob': 7.2.0 - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.2.12 - glob: 7.2.3 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 3.0.0 - dev: true - /globby@11.0.1: resolution: {integrity: sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==} engines: {node: '>=10'} @@ -13755,10 +13452,6 @@ packages: pinkie-promise: 2.0.1 dev: false - /globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - dev: true - /gonzales-pe@4.3.0: resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==} engines: {node: '>=0.6.0'} @@ -14180,13 +13873,6 @@ packages: resolution: {integrity: sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA==} dev: false - /html-encoding-sniffer@2.0.1: - resolution: {integrity: sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==} - engines: {node: '>=10'} - dependencies: - whatwg-encoding: 1.0.5 - dev: true - /html-encoding-sniffer@3.0.0: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} @@ -14373,11 +14059,6 @@ packages: transitivePeerDependencies: - supports-color - /human-signals@1.1.1: - resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} - engines: {node: '>=8.12.0'} - dev: true - /human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -14388,10 +14069,6 @@ packages: engines: {node: '>=14.18.0'} dev: true - /humanize-duration@3.30.0: - resolution: {integrity: sha512-NxpT0fhQTFuMTLnuu1Xp+ozNpYirQnbV3NlOjEKBYlE3uvMRu3LDuq8EPc3gVXxVYnchQfqVM4/+T9iwHPLLeA==} - dev: true - /humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} dependencies: @@ -14628,11 +14305,6 @@ packages: engines: {node: '>=12'} dev: false - /interpret@1.4.0: - resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} - engines: {node: '>= 0.10'} - dev: true - /intersection-observer@0.12.2: resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} dev: false @@ -14727,7 +14399,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==} @@ -15143,10 +14814,6 @@ packages: gopd: 1.0.1 has-tostringtag: 1.0.0 - /is-typedarray@1.0.0: - resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - dev: true - /is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -15294,15 +14961,6 @@ packages: resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==} dev: true - /jest-changed-files@27.5.1: - resolution: {integrity: sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/types': 27.5.1 - execa: 5.1.1 - throat: 6.0.2 - dev: true - /jest-changed-files@29.5.0: resolution: {integrity: sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15311,33 +14969,6 @@ packages: p-limit: 3.1.0 dev: true - /jest-circus@27.5.1: - resolution: {integrity: sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/environment': 27.5.1 - '@jest/test-result': 27.5.1 - '@jest/types': 27.5.1 - '@types/node': 17.0.23 - chalk: 4.1.2 - co: 4.6.0 - dedent: 0.7.0 - expect: 27.5.1 - is-generator-fn: 2.1.0 - jest-each: 27.5.1 - jest-matcher-utils: 27.5.1 - jest-message-util: 27.5.1 - jest-runtime: 27.5.1 - jest-snapshot: 27.5.1 - jest-util: 27.5.1 - pretty-format: 27.5.1 - slash: 3.0.0 - stack-utils: 2.0.6 - throat: 6.0.2 - transitivePeerDependencies: - - supports-color - dev: true - /jest-circus@29.5.0: resolution: {integrity: sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15346,7 +14977,7 @@ packages: '@jest/expect': 29.5.0 '@jest/test-result': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 17.0.23 + '@types/node': 20.1.2 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 @@ -15366,36 +14997,6 @@ packages: - supports-color dev: true - /jest-cli@27.5.1(ts-node@10.9.1): - resolution: {integrity: sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 27.5.1(ts-node@10.9.1) - '@jest/test-result': 27.5.1 - '@jest/types': 27.5.1 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - import-local: 3.1.0 - jest-config: 27.5.1(ts-node@10.9.1) - jest-util: 27.5.1 - jest-validate: 27.5.1 - prompts: 2.4.2 - yargs: 16.2.0 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - ts-node - - utf-8-validate - dev: true - /jest-cli@29.5.0(@types/node@17.0.23): resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15424,48 +15025,46 @@ packages: - ts-node dev: true - /jest-config@27.5.1(ts-node@10.9.1): - resolution: {integrity: sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + /jest-config@29.5.0(@types/node@17.0.23): + resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: + '@types/node': '*' ts-node: '>=9.0.0' peerDependenciesMeta: + '@types/node': + optional: true ts-node: optional: true dependencies: '@babel/core': 7.22.5 - '@jest/test-sequencer': 27.5.1 - '@jest/types': 27.5.1 - babel-jest: 27.5.1(@babel/core@7.22.5) + '@jest/test-sequencer': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 17.0.23 + babel-jest: 29.5.0(@babel/core@7.22.5) chalk: 4.1.2 ci-info: 3.8.0 deepmerge: 4.3.1 glob: 7.2.3 graceful-fs: 4.2.11 - jest-circus: 27.5.1 - jest-environment-jsdom: 27.5.1 - jest-environment-node: 27.5.1 - jest-get-type: 27.5.1 - jest-jasmine2: 27.5.1 - jest-regex-util: 27.5.1 - jest-resolve: 27.5.1 - jest-runner: 27.5.1 - jest-util: 27.5.1 - jest-validate: 27.5.1 + jest-circus: 29.5.0 + jest-environment-node: 29.5.0 + jest-get-type: 29.4.3 + jest-regex-util: 29.4.3 + jest-resolve: 29.5.0 + jest-runner: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 27.5.1 + pretty-format: 29.5.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@17.0.23)(typescript@4.9.5) transitivePeerDependencies: - - bufferutil - - canvas - supports-color - - utf-8-validate dev: true - /jest-config@29.5.0(@types/node@17.0.23): + /jest-config@29.5.0(@types/node@20.1.2): resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -15480,7 +15079,7 @@ packages: '@babel/core': 7.22.5 '@jest/test-sequencer': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 17.0.23 + '@types/node': 20.1.2 babel-jest: 29.5.0(@babel/core@7.22.5) chalk: 4.1.2 ci-info: 3.8.0 @@ -15504,16 +15103,6 @@ packages: - supports-color dev: true - /jest-diff@27.5.1: - resolution: {integrity: sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - chalk: 4.1.2 - diff-sequences: 27.5.1 - jest-get-type: 27.5.1 - pretty-format: 27.5.1 - dev: true - /jest-diff@29.5.0: resolution: {integrity: sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15524,13 +15113,6 @@ packages: pretty-format: 29.5.0 dev: true - /jest-docblock@27.5.1: - resolution: {integrity: sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - detect-newline: 3.1.0 - dev: true - /jest-docblock@29.4.3: resolution: {integrity: sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15538,17 +15120,6 @@ packages: detect-newline: 3.1.0 dev: true - /jest-each@27.5.1: - resolution: {integrity: sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/types': 27.5.1 - chalk: 4.1.2 - jest-get-type: 27.5.1 - jest-util: 27.5.1 - pretty-format: 27.5.1 - dev: true - /jest-each@29.5.0: resolution: {integrity: sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15560,42 +15131,6 @@ packages: pretty-format: 29.5.0 dev: true - /jest-environment-jsdom@27.0.0: - resolution: {integrity: sha512-TjkjbYELkYC4IiIOlwKreZv2K8KuKh8n+lM3CUFvxrJrVlybDlYGhtV8fTVQiWwXVij4SipyMnQDI+KxNdd7Cw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/environment': 27.5.1 - '@jest/fake-timers': 27.5.1 - '@jest/types': 27.5.1 - '@types/node': 17.0.23 - jest-mock: 27.5.1 - jest-util: 27.5.1 - jsdom: 16.7.0 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - utf-8-validate - dev: true - - /jest-environment-jsdom@27.5.1: - resolution: {integrity: sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/environment': 27.5.1 - '@jest/fake-timers': 27.5.1 - '@jest/types': 27.5.1 - '@types/node': 17.0.23 - jest-mock: 27.5.1 - jest-util: 27.5.1 - jsdom: 16.7.0 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - utf-8-validate - dev: true - /jest-environment-jsdom@29.5.0: resolution: {integrity: sha512-/KG8yEK4aN8ak56yFVdqFDzKNHgF4BAymCx2LbPNPsUshUlfAl0eX402Xm1pt+eoG9SLZEUVifqXtX8SK74KCw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15609,7 +15144,7 @@ packages: '@jest/fake-timers': 29.5.0 '@jest/types': 29.5.0 '@types/jsdom': 20.0.1 - '@types/node': 17.0.23 + '@types/node': 20.1.2 jest-mock: 29.5.0 jest-util: 29.5.0 jsdom: 20.0.3 @@ -15619,18 +15154,6 @@ packages: - utf-8-validate dev: true - /jest-environment-node@27.5.1: - resolution: {integrity: sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/environment': 27.5.1 - '@jest/fake-timers': 27.5.1 - '@jest/types': 27.5.1 - '@types/node': 17.0.23 - jest-mock: 27.5.1 - jest-util: 27.5.1 - dev: true - /jest-environment-node@29.5.0: resolution: {integrity: sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15638,7 +15161,7 @@ packages: '@jest/environment': 29.5.0 '@jest/fake-timers': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 17.0.23 + '@types/node': 20.1.2 jest-mock: 29.5.0 jest-util: 29.5.0 dev: true @@ -15648,43 +15171,18 @@ packages: engines: {node: '>= 10.14.2'} dev: false - /jest-get-type@27.5.1: - resolution: {integrity: sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dev: true - /jest-get-type@29.4.3: resolution: {integrity: sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /jest-haste-map@27.5.1: - resolution: {integrity: sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/types': 27.5.1 - '@types/graceful-fs': 4.1.6 - '@types/node': 17.0.23 - anymatch: 3.1.3 - fb-watchman: 2.0.2 - graceful-fs: 4.2.11 - jest-regex-util: 27.5.1 - jest-serializer: 27.5.1 - jest-util: 27.5.1 - jest-worker: 27.5.1 - micromatch: 4.0.5 - walker: 1.0.8 - optionalDependencies: - fsevents: 2.3.2 - dev: true - /jest-haste-map@29.5.0: resolution: {integrity: sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.5.0 '@types/graceful-fs': 4.1.6 - '@types/node': 17.0.23 + '@types/node': 20.1.2 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -15697,39 +15195,6 @@ packages: fsevents: 2.3.2 dev: true - /jest-jasmine2@27.5.1: - resolution: {integrity: sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/environment': 27.5.1 - '@jest/source-map': 27.5.1 - '@jest/test-result': 27.5.1 - '@jest/types': 27.5.1 - '@types/node': 17.0.23 - chalk: 4.1.2 - co: 4.6.0 - expect: 27.5.1 - is-generator-fn: 2.1.0 - jest-each: 27.5.1 - jest-matcher-utils: 27.5.1 - jest-message-util: 27.5.1 - jest-runtime: 27.5.1 - jest-snapshot: 27.5.1 - jest-util: 27.5.1 - pretty-format: 27.5.1 - throat: 6.0.2 - transitivePeerDependencies: - - supports-color - dev: true - - /jest-leak-detector@27.5.1: - resolution: {integrity: sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - jest-get-type: 27.5.1 - pretty-format: 27.5.1 - dev: true - /jest-leak-detector@29.5.0: resolution: {integrity: sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15738,16 +15203,6 @@ packages: pretty-format: 29.5.0 dev: true - /jest-matcher-utils@27.5.1: - resolution: {integrity: sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - chalk: 4.1.2 - jest-diff: 27.5.1 - jest-get-type: 27.5.1 - pretty-format: 27.5.1 - dev: true - /jest-matcher-utils@29.5.0: resolution: {integrity: sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15758,21 +15213,6 @@ packages: pretty-format: 29.5.0 dev: true - /jest-message-util@27.5.1: - resolution: {integrity: sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@babel/code-frame': 7.22.5 - '@jest/types': 27.5.1 - '@types/stack-utils': 2.0.1 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.5 - pretty-format: 27.5.1 - slash: 3.0.0 - stack-utils: 2.0.6 - dev: true - /jest-message-util@29.5.0: resolution: {integrity: sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15784,37 +15224,17 @@ packages: graceful-fs: 4.2.11 micromatch: 4.0.5 pretty-format: 29.5.0 - slash: 3.0.0 - stack-utils: 2.0.6 - dev: true - - /jest-mock@27.5.1: - resolution: {integrity: sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/types': 27.5.1 - '@types/node': 17.0.23 - dev: true - - /jest-mock@29.5.0: - resolution: {integrity: sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/types': 29.5.0 - '@types/node': 17.0.23 - jest-util: 29.5.0 - dev: true - - /jest-pnp-resolver@1.2.3(jest-resolve@27.5.1): - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true + slash: 3.0.0 + stack-utils: 2.0.6 + dev: true + + /jest-mock@29.5.0: + resolution: {integrity: sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-resolve: 27.5.1 + '@jest/types': 29.5.0 + '@types/node': 20.1.2 + jest-util: 29.5.0 dev: true /jest-pnp-resolver@1.2.3(jest-resolve@29.5.0): @@ -15832,23 +15252,13 @@ packages: /jest-regex-util@27.5.1: resolution: {integrity: sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dev: false /jest-regex-util@29.4.3: resolution: {integrity: sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /jest-resolve-dependencies@27.5.1: - resolution: {integrity: sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/types': 27.5.1 - jest-regex-util: 27.5.1 - jest-snapshot: 27.5.1 - transitivePeerDependencies: - - supports-color - dev: true - /jest-resolve-dependencies@29.5.0: resolution: {integrity: sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15859,22 +15269,6 @@ packages: - supports-color dev: true - /jest-resolve@27.5.1: - resolution: {integrity: sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/types': 27.5.1 - chalk: 4.1.2 - graceful-fs: 4.2.11 - jest-haste-map: 27.5.1 - jest-pnp-resolver: 1.2.3(jest-resolve@27.5.1) - jest-util: 27.5.1 - jest-validate: 27.5.1 - resolve: 1.22.2 - resolve.exports: 1.1.1 - slash: 3.0.0 - dev: true - /jest-resolve@29.5.0: resolution: {integrity: sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15890,38 +15284,6 @@ packages: slash: 3.0.0 dev: true - /jest-runner@27.5.1: - resolution: {integrity: sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/console': 27.5.1 - '@jest/environment': 27.5.1 - '@jest/test-result': 27.5.1 - '@jest/transform': 27.5.1 - '@jest/types': 27.5.1 - '@types/node': 17.0.23 - chalk: 4.1.2 - emittery: 0.8.1 - graceful-fs: 4.2.11 - jest-docblock: 27.5.1 - jest-environment-jsdom: 27.5.1 - jest-environment-node: 27.5.1 - jest-haste-map: 27.5.1 - jest-leak-detector: 27.5.1 - jest-message-util: 27.5.1 - jest-resolve: 27.5.1 - jest-runtime: 27.5.1 - jest-util: 27.5.1 - jest-worker: 27.5.1 - source-map-support: 0.5.21 - throat: 6.0.2 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - utf-8-validate - dev: true - /jest-runner@29.5.0: resolution: {integrity: sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15931,7 +15293,7 @@ packages: '@jest/test-result': 29.5.0 '@jest/transform': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 17.0.23 + '@types/node': 20.1.2 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -15951,36 +15313,6 @@ packages: - supports-color dev: true - /jest-runtime@27.5.1: - resolution: {integrity: sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/environment': 27.5.1 - '@jest/fake-timers': 27.5.1 - '@jest/globals': 27.5.1 - '@jest/source-map': 27.5.1 - '@jest/test-result': 27.5.1 - '@jest/transform': 27.5.1 - '@jest/types': 27.5.1 - chalk: 4.1.2 - cjs-module-lexer: 1.2.2 - collect-v8-coverage: 1.0.1 - execa: 5.1.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-haste-map: 27.5.1 - jest-message-util: 27.5.1 - jest-mock: 27.5.1 - jest-regex-util: 27.5.1 - jest-resolve: 27.5.1 - jest-snapshot: 27.5.1 - jest-util: 27.5.1 - slash: 3.0.0 - strip-bom: 4.0.0 - transitivePeerDependencies: - - supports-color - dev: true - /jest-runtime@29.5.0: resolution: {integrity: sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15992,7 +15324,7 @@ packages: '@jest/test-result': 29.5.0 '@jest/transform': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 17.0.23 + '@types/node': 20.1.2 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 @@ -16015,38 +15347,9 @@ packages: resolution: {integrity: sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: - '@types/node': 17.0.23 - graceful-fs: 4.2.11 - - /jest-snapshot@27.5.1: - resolution: {integrity: sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@babel/core': 7.22.5 - '@babel/generator': 7.22.5 - '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.22.5) - '@babel/traverse': 7.22.5 - '@babel/types': 7.22.11 - '@jest/transform': 27.5.1 - '@jest/types': 27.5.1 - '@types/babel__traverse': 7.18.5 - '@types/prettier': 2.7.2 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.5) - chalk: 4.1.2 - expect: 27.5.1 + '@types/node': 20.1.2 graceful-fs: 4.2.11 - jest-diff: 27.5.1 - jest-get-type: 27.5.1 - jest-haste-map: 27.5.1 - jest-matcher-utils: 27.5.1 - jest-message-util: 27.5.1 - jest-util: 27.5.1 - natural-compare: 1.4.0 - pretty-format: 27.5.1 - semver: 7.5.4 - transitivePeerDependencies: - - supports-color - dev: true + dev: false /jest-snapshot@29.5.0: resolution: {integrity: sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==} @@ -16084,18 +15387,19 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 17.0.23 + '@types/node': 20.1.2 chalk: 4.1.2 ci-info: 3.8.0 graceful-fs: 4.2.11 picomatch: 2.3.1 + dev: false /jest-util@29.5.0: resolution: {integrity: sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.5.0 - '@types/node': 17.0.23 + '@types/node': 20.1.2 chalk: 4.1.2 ci-info: 3.8.0 graceful-fs: 4.2.11 @@ -16114,18 +15418,6 @@ packages: pretty-format: 26.6.2 dev: false - /jest-validate@27.5.1: - resolution: {integrity: sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/types': 27.5.1 - camelcase: 6.3.0 - chalk: 4.1.2 - jest-get-type: 27.5.1 - leven: 3.1.0 - pretty-format: 27.5.1 - dev: true - /jest-validate@29.5.0: resolution: {integrity: sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -16138,42 +15430,13 @@ packages: pretty-format: 29.5.0 dev: true - /jest-watch-typeahead@0.6.5(jest@27.5.1): - resolution: {integrity: sha512-GIbV6h37/isatMDtqZlA8Q5vC6T3w+5qdvtF+3LIkPc58zEWzbKmTHvlUIp3wvBm400RzrQWcVPcsAJqKWu7XQ==} - engines: {node: '>=10'} - peerDependencies: - jest: ^26.0.0 || ^27.0.0 - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - jest: 27.5.1(ts-node@10.9.1) - jest-regex-util: 27.5.1 - jest-watcher: 27.5.1 - slash: 3.0.0 - string-length: 4.0.2 - strip-ansi: 6.0.1 - dev: true - - /jest-watcher@27.5.1: - resolution: {integrity: sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/test-result': 27.5.1 - '@jest/types': 27.5.1 - '@types/node': 17.0.23 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - jest-util: 27.5.1 - string-length: 4.0.2 - dev: true - /jest-watcher@29.5.0: resolution: {integrity: sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/test-result': 29.5.0 '@jest/types': 29.5.0 - '@types/node': 17.0.23 + '@types/node': 20.1.2 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -16185,15 +15448,16 @@ packages: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 17.0.23 + '@types/node': 20.1.2 merge-stream: 2.0.0 supports-color: 7.2.0 + dev: false /jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 17.0.23 + '@types/node': 20.1.2 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -16201,54 +15465,12 @@ packages: resolution: {integrity: sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 17.0.23 + '@types/node': 20.1.2 jest-util: 29.5.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest@27.4.5: - resolution: {integrity: sha512-uT5MiVN3Jppt314kidCk47MYIRilJjA/l2mxwiuzzxGUeJIvA8/pDaJOAX5KWvjAo7SCydcW0/4WEtgbLMiJkg==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 27.5.1(ts-node@10.9.1) - import-local: 3.1.0 - jest-cli: 27.5.1(ts-node@10.9.1) - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - ts-node - - utf-8-validate - dev: true - - /jest@27.5.1(ts-node@10.9.1): - resolution: {integrity: sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 27.5.1(ts-node@10.9.1) - import-local: 3.1.0 - jest-cli: 27.5.1(ts-node@10.9.1) - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - ts-node - - utf-8-validate - dev: true - /jest@29.5.0(@types/node@17.0.23): resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -16295,8 +15517,9 @@ packages: resolution: {integrity: sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==} dev: false - /jpjs@1.2.1: - resolution: {integrity: sha512-GxJWybWU4NV0RNKi6EIqk6IRPOTqd/h+U7sbtyuD7yUISUzV78LdHnq2xkevJsTlz/EImux4sWj+wfMiwKLkiw==} + /joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} dev: true /js-string-escape@1.0.1: @@ -16354,48 +15577,6 @@ packages: - supports-color dev: false - /jsdom@16.7.0: - resolution: {integrity: sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==} - engines: {node: '>=10'} - peerDependencies: - canvas: ^2.5.0 - peerDependenciesMeta: - canvas: - optional: true - dependencies: - abab: 2.0.6 - acorn: 8.10.0 - acorn-globals: 6.0.0 - cssom: 0.4.4 - cssstyle: 2.3.0 - data-urls: 2.0.0 - decimal.js: 10.4.3 - domexception: 2.0.1 - escodegen: 2.0.0 - form-data: 3.0.1 - html-encoding-sniffer: 2.0.1 - http-proxy-agent: 4.0.1 - https-proxy-agent: 5.0.1 - is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.4 - parse5: 6.0.1 - saxes: 5.0.1 - symbol-tree: 3.2.4 - tough-cookie: 4.1.2 - w3c-hr-time: 1.0.2 - w3c-xmlserializer: 2.0.0 - webidl-conversions: 6.1.0 - whatwg-encoding: 1.0.5 - whatwg-mimetype: 2.3.0 - whatwg-url: 8.7.0 - ws: 7.5.9 - xml-name-validator: 3.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: true - /jsdom@20.0.3: resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} engines: {node: '>=14'} @@ -16813,6 +15994,11 @@ packages: type-fest: 0.6.0 dev: true + /load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /loader-runner@2.4.0: resolution: {integrity: sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==} engines: {node: '>=4.3.0 <5.0.0 || >=5.10'} @@ -16873,7 +16059,6 @@ packages: dependencies: p-locate: 3.0.0 path-exists: 3.0.0 - dev: false /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} @@ -16916,13 +16101,17 @@ packages: /lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + dev: false /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + /lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + dev: true + /lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} - dev: false /lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} @@ -16945,15 +16134,6 @@ packages: chalk: 4.1.2 is-unicode-supported: 0.1.0 - /log-update@2.3.0: - resolution: {integrity: sha512-vlP11XfFGyeNQlmEn9tJ66rEW1coA/79m5z6BCkudjbAGE83uhAcGYrBFwfs3AdLiLzGRusRPAbSPK9xZteCmg==} - engines: {node: '>=4'} - dependencies: - ansi-escapes: 3.2.0 - cli-cursor: 2.1.0 - wrap-ansi: 3.0.1 - dev: true - /logkitty@0.7.1: resolution: {integrity: sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ==} hasBin: true @@ -16987,6 +16167,7 @@ packages: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: tslib: 2.5.0 + dev: false /lowercase-keys@2.0.0: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} @@ -17032,15 +16213,15 @@ packages: sourcemap-codec: 1.4.8 dev: true - /magic-string@0.26.7: - resolution: {integrity: sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==} + /magic-string@0.30.0: + resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} engines: {node: '>=12'} dependencies: - sourcemap-codec: 1.4.8 + '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /magic-string@0.30.0: - resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} + /magic-string@0.30.5: + resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} engines: {node: '>=12'} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -17059,10 +16240,6 @@ packages: dependencies: semver: 6.3.1 - /make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - dev: true - /make-fetch-happen@10.2.1: resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -18296,6 +17473,7 @@ packages: /mimic-fn@1.2.0: resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} engines: {node: '>=4'} + dev: false /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} @@ -18665,6 +17843,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 @@ -18891,12 +18073,20 @@ packages: dependencies: lower-case: 2.0.2 tslib: 2.5.0 + dev: false /nocache@3.0.4: resolution: {integrity: sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==} 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 @@ -18907,6 +18097,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'} @@ -19523,6 +18717,7 @@ packages: engines: {node: '>=4'} dependencies: mimic-fn: 1.2.0 + dev: false /onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} @@ -19717,7 +18912,6 @@ packages: engines: {node: '>=6'} dependencies: p-limit: 2.3.0 - dev: false /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} @@ -19741,13 +18935,6 @@ packages: engines: {node: '>=6'} dev: false - /p-map@3.0.0: - resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} - engines: {node: '>=8'} - dependencies: - aggregate-error: 3.1.0 - dev: true - /p-map@4.0.0: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} @@ -19964,10 +19151,6 @@ packages: dependencies: parse-path: 7.0.0 - /parse5@6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - dev: true - /parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} dependencies: @@ -19982,6 +19165,7 @@ packages: dependencies: no-case: 3.0.4 tslib: 2.5.0 + dev: false /pascalcase@0.1.1: resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} @@ -20023,12 +19207,6 @@ packages: resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} dev: false - /path-is-network-drive@1.0.20: - resolution: {integrity: sha512-p5wCWlRB4+ggzxWshqHH9aF3kAuVu295NaENXmVhThbZPJQBeJdxZTP6CIoUR+kWHDUW56S9YcaO1gXnc/BOxw==} - dependencies: - tslib: 2.5.0 - dev: true - /path-key@2.0.1: resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} engines: {node: '>=4'} @@ -20062,12 +19240,6 @@ packages: minipass: 5.0.0 dev: true - /path-strip-sep@1.0.17: - resolution: {integrity: sha512-+2zIC2fNgdilgV7pTrktY6oOxxZUo9x5zJYfTzxsGze5kSGDDwhA5/0WlBn+sUyv/WuuyYn3OfM+Ue5nhdQUgA==} - dependencies: - tslib: 2.5.0 - dev: true - /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} @@ -20182,13 +19354,6 @@ packages: dependencies: find-up: 4.1.0 - /pkg-dir@5.0.0: - resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} - engines: {node: '>=10'} - dependencies: - find-up: 5.0.0 - dev: true - /pkg-types@1.0.3: resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} dependencies: @@ -20228,11 +19393,11 @@ packages: engines: {node: '>=4.0.0'} dev: false - /pnp-webpack-plugin@1.7.0(typescript@5.0.4): + /pnp-webpack-plugin@1.7.0(typescript@5.2.2): resolution: {integrity: sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg==} engines: {node: '>=6'} dependencies: - ts-pnp: 1.2.0(typescript@5.0.4) + ts-pnp: 1.2.0(typescript@5.2.2) transitivePeerDependencies: - typescript dev: false @@ -20791,6 +19956,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'} @@ -20952,15 +20136,6 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} - /progress-estimator@0.3.1: - resolution: {integrity: sha512-I5bwE35adOrA2rfZ9iHHPESUp5C6cXrZd0J8LdFD9J+66ijSugBwZHooPCROf94Ox8YZaUyvTLViqSiRvBhSoA==} - dependencies: - chalk: 2.4.2 - cli-spinners: 1.3.1 - humanize-duration: 3.30.0 - log-update: 2.3.0 - dev: true - /progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -21182,6 +20357,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: @@ -21240,9 +20419,8 @@ 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): + /react-dev-utils@11.0.4(typescript@5.2.2)(webpack@4.43.0): resolution: {integrity: sha512-dx0LvIGHcOPtKbeiSUM4jqpBl3TcY7CDjZdfOIcKeznE7BWr9dg0iPG90G5yfVQ+p/rGNMXdbfStvzQZEVEi4A==} engines: {node: '>=10'} peerDependencies: @@ -21261,7 +20439,7 @@ packages: escape-string-regexp: 2.0.0 filesize: 6.1.0 find-up: 4.1.0 - fork-ts-checker-webpack-plugin: 4.1.6(typescript@5.0.4)(webpack@4.43.0) + fork-ts-checker-webpack-plugin: 4.1.6(typescript@5.2.2)(webpack@4.43.0) global-modules: 2.0.0 globby: 11.0.1 gzip-size: 5.1.1 @@ -21276,7 +20454,7 @@ packages: shell-quote: 1.7.2 strip-ansi: 6.0.0 text-table: 0.2.0 - typescript: 5.0.4 + typescript: 5.2.2 webpack: 4.43.0 transitivePeerDependencies: - eslint @@ -21652,13 +20830,6 @@ packages: tslib: 2.5.0 dev: true - /rechoir@0.6.2: - resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} - engines: {node: '>= 0.10'} - dependencies: - resolve: 1.22.2 - dev: true - /recursive-readdir@2.2.2: resolution: {integrity: sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==} engines: {node: '>=0.10.0'} @@ -21930,7 +21101,6 @@ packages: /require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} - dev: false /requireg@0.2.2: resolution: {integrity: sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==} @@ -21985,11 +21155,6 @@ packages: deprecated: https://github.com/lydell/resolve-url#deprecated dev: false - /resolve.exports@1.1.1: - resolution: {integrity: sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==} - engines: {node: '>=10'} - dev: true - /resolve.exports@2.0.2: resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} engines: {node: '>=10'} @@ -22030,6 +21195,7 @@ packages: dependencies: onetime: 2.0.1 signal-exit: 3.0.7 + dev: false /restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} @@ -22112,27 +21278,6 @@ packages: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} dev: false - /rollup-plugin-delete@2.0.0: - resolution: {integrity: sha512-/VpLMtDy+8wwRlDANuYmDa9ss/knGsAgrDhM+tEwB1npHwNu4DYNmDfUL55csse/GHs9Q+SMT/rw9uiaZ3pnzA==} - engines: {node: '>=10'} - dependencies: - del: 5.1.0 - dev: true - - /rollup-plugin-dts@4.2.3(rollup@2.79.1)(typescript@4.9.5): - resolution: {integrity: sha512-jlcpItqM2efqfIiKzDB/IKOS9E9fDvbkJSGw5GtK/PqPGS9eC3R3JKyw2VvpTktZA+TNgJRMu1NTv244aTUzzQ==} - engines: {node: '>=v12.22.12'} - peerDependencies: - rollup: ^2.55 - typescript: ^4.1 - dependencies: - magic-string: 0.26.7 - rollup: 2.79.1 - typescript: 4.9.5 - optionalDependencies: - '@babel/code-frame': 7.22.5 - dev: true - /rollup-plugin-inject@3.0.2: resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. @@ -22148,49 +21293,13 @@ packages: rollup-plugin-inject: 3.0.2 dev: true - /rollup-plugin-sourcemaps@0.6.3(@types/node@17.0.23)(rollup@2.79.1): - resolution: {integrity: sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw==} - engines: {node: '>=10.0.0'} - peerDependencies: - '@types/node': '>=10.0.0' - rollup: '>=0.31.2' - peerDependenciesMeta: - '@types/node': - optional: true - dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.79.1) - '@types/node': 17.0.23 - rollup: 2.79.1 - source-map-resolve: 0.6.0 - dev: true - - /rollup-plugin-terser@7.0.2(rollup@2.79.1): - resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} - deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser - peerDependencies: - rollup: ^2.0.0 - dependencies: - '@babel/code-frame': 7.22.5 - jest-worker: 26.6.2 - rollup: 2.79.1 - serialize-javascript: 4.0.0 - terser: 5.18.2 - dev: true - - /rollup-plugin-typescript2@0.31.2(rollup@2.79.1)(typescript@4.9.5): - resolution: {integrity: sha512-hRwEYR1C8xDGVVMFJQdEVnNAeWRvpaY97g5mp3IeLnzhNXzSVq78Ye/BJ9PAaUfN4DXa/uDnqerifMOaMFY54Q==} + /rollup-plugin-preserve-directives@0.2.0(rollup@3.28.1): + resolution: {integrity: sha512-KUwbBaFvD1zFIDNnOkR+u64sSod3m0l6q46/SzTOa4GTQ6hp6w0FRr2u7x99YkY9qhlna5panmTmuLWeJ/2KWw==} peerDependencies: - rollup: '>=1.26.3' - typescript: '>=2.4.0' + rollup: 2.x || 3.x dependencies: - '@rollup/pluginutils': 4.2.1 - '@yarn-tool/resolve-package': 1.0.47 - find-cache-dir: 3.3.2 - fs-extra: 10.1.0 - resolve: 1.22.2 - rollup: 2.79.1 - tslib: 2.5.0 - typescript: 4.9.5 + magic-string: 0.30.0 + rollup: 3.28.1 dev: true /rollup-pluginutils@2.8.2: @@ -22199,14 +21308,6 @@ packages: estree-walker: 0.6.1 dev: true - /rollup@2.79.1: - resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} - engines: {node: '>=10.0.0'} - hasBin: true - optionalDependencies: - fsevents: 2.3.2 - dev: true - /rollup@3.28.1: resolution: {integrity: sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -22295,13 +21396,6 @@ packages: resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} dev: false - /saxes@5.0.1: - resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} - engines: {node: '>=10'} - dependencies: - xmlchars: 2.2.0 - dev: true - /saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -22468,6 +21562,7 @@ packages: resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} dependencies: randombytes: 2.1.0 + dev: false /serialize-javascript@6.0.1: resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} @@ -22546,6 +21641,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'} @@ -22576,16 +21686,6 @@ packages: resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} dev: false - /shelljs@0.8.5: - resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} - engines: {node: '>=4'} - hasBin: true - dependencies: - glob: 7.2.3 - interpret: 1.4.0 - rechoir: 0.6.2 - dev: true - /shiki@0.14.5: resolution: {integrity: sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==} dependencies: @@ -22626,6 +21726,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: @@ -22638,7 +21750,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==} @@ -22684,6 +21795,10 @@ packages: engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} dev: true + /smob@1.4.1: + resolution: {integrity: sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==} + dev: true + /snapdragon-node@2.1.1: resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} engines: {node: '>=0.10.0'} @@ -22816,14 +21931,6 @@ packages: urix: 0.1.0 dev: false - /source-map-resolve@0.6.0: - resolution: {integrity: sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==} - deprecated: See https://github.com/lydell/source-map-resolve#deprecated - dependencies: - atob: 2.1.2 - decode-uri-component: 0.2.2 - dev: true - /source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} dependencies: @@ -22855,6 +21962,13 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} + /source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + dependencies: + whatwg-url: 7.1.0 + dev: true + /sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} deprecated: Please use @jridgewell/sourcemap-codec instead @@ -23052,6 +22166,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 @@ -23064,18 +22185,6 @@ packages: strip-ansi: 6.0.1 dev: true - /string-natural-compare@3.0.1: - resolution: {integrity: sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==} - dev: true - - /string-width@2.1.1: - resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} - engines: {node: '>=4'} - dependencies: - is-fullwidth-code-point: 2.0.0 - strip-ansi: 4.0.0 - dev: true - /string-width@3.1.0: resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==} engines: {node: '>=6'} @@ -23083,7 +22192,6 @@ packages: emoji-regex: 7.0.3 is-fullwidth-code-point: 2.0.0 strip-ansi: 5.2.0 - dev: false /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -23164,19 +22272,11 @@ packages: ansi-regex: 2.1.1 dev: false - /strip-ansi@4.0.0: - resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} - engines: {node: '>=4'} - dependencies: - ansi-regex: 3.0.1 - dev: true - /strip-ansi@5.2.0: resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} engines: {node: '>=6'} dependencies: ansi-regex: 4.1.1 - dev: false /strip-ansi@6.0.0: resolution: {integrity: sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==} @@ -23238,7 +22338,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==} @@ -23377,6 +22476,7 @@ packages: dependencies: has-flag: 4.0.0 supports-color: 7.2.0 + dev: false /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} @@ -23496,6 +22596,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'} @@ -23507,6 +22615,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'} @@ -23590,6 +22706,7 @@ packages: dependencies: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 + dev: false /terser-webpack-plugin@1.4.5(webpack@4.43.0): resolution: {integrity: sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==} @@ -23629,7 +22746,7 @@ packages: - bluebird dev: false - /terser-webpack-plugin@5.3.9(webpack@5.88.1): + /terser-webpack-plugin@5.3.9(esbuild@0.18.20)(webpack@5.88.1): resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} engines: {node: '>= 10.13.0'} peerDependencies: @@ -23646,11 +22763,12 @@ packages: optional: true dependencies: '@jridgewell/trace-mapping': 0.3.18 + esbuild: 0.18.20 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.1 terser: 5.18.2 - webpack: 5.88.1 + webpack: 5.88.1(esbuild@0.18.20) dev: true /terser@4.8.1: @@ -23707,10 +22825,6 @@ packages: resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} dev: false - /throat@6.0.2: - resolution: {integrity: sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==} - dev: true - /through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} dependencies: @@ -23746,13 +22860,6 @@ packages: resolution: {integrity: sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==} dev: false - /tiny-glob@0.2.9: - resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} - dependencies: - globalyzer: 0.1.0 - globrex: 0.1.2 - dev: true - /tinybench@2.5.0: resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} dev: true @@ -23863,9 +22970,8 @@ packages: /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - /tr46@2.1.0: - resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==} - engines: {node: '>=8'} + /tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} dependencies: punycode: 2.3.0 dev: true @@ -23881,6 +22987,11 @@ packages: resolution: {integrity: sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==} dev: false + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + /treeverse@3.0.0: resolution: {integrity: sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -23898,13 +23009,13 @@ packages: /trough@2.1.0: resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} - /ts-api-utils@1.0.2(typescript@5.0.4): + /ts-api-utils@1.0.2(typescript@5.2.2): resolution: {integrity: sha512-Cbu4nIqnEdd+THNEsBdkolnOXhg0I8XteoHaEKgvsxpsbWda4IsUut2c187HxywQCvveojow0Dgw/amxtSKVkQ==} engines: {node: '>=16.13.0'} peerDependencies: typescript: '>=4.2.0' dependencies: - typescript: 5.0.4 + typescript: 5.2.2 dev: true /ts-dedent@2.2.0: @@ -23915,73 +23026,7 @@ packages: /ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - /ts-jest@27.1.5(@babel/core@7.22.5)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5): - resolution: {integrity: sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@types/jest': ^27.0.0 - babel-jest: '>=27.0.0 <28' - esbuild: '*' - jest: ^27.0.0 - typescript: '>=3.8 <5.0' - peerDependenciesMeta: - '@babel/core': - optional: true - '@types/jest': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - dependencies: - '@babel/core': 7.22.5 - '@types/jest': 27.5.2 - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - jest: 27.5.1(ts-node@10.9.1) - jest-util: 27.5.1 - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.5.4 - typescript: 4.9.5 - yargs-parser: 20.2.9 - dev: true - - /ts-node@10.9.1(@types/node@17.0.23)(typescript@4.9.5): - resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.9 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 17.0.23 - acorn: 8.10.0 - acorn-walk: 8.2.0 - arg: 4.1.0 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 4.9.5 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - dev: true - - /ts-pnp@1.2.0(typescript@5.0.4): + /ts-pnp@1.2.0(typescript@5.2.2): resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==} engines: {node: '>=6'} peerDependencies: @@ -23990,7 +23035,7 @@ packages: typescript: optional: true dependencies: - typescript: 5.0.4 + typescript: 5.2.2 dev: false /tsconfig-paths@3.14.2: @@ -24022,24 +23067,49 @@ packages: /tslib@2.5.0: resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} - /tsutils@3.21.0(typescript@4.9.5): - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - engines: {node: '>= 6'} + /tsup@7.2.0: + resolution: {integrity: sha512-vDHlczXbgUvY3rWvqFEbSqmC1L7woozbzngMqTtL2PGBODTtWlRwGDDawhvWzr5c1QjKe4OAKqJGfE1xeXUvtQ==} + engines: {node: '>=16.14'} + hasBin: true peerDependencies: - typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.1.0' + peerDependenciesMeta: + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true dependencies: - tslib: 1.14.1 - typescript: 4.9.5 + bundle-require: 4.0.2(esbuild@0.18.20) + cac: 6.7.14 + chokidar: 3.5.3 + debug: 4.3.4(supports-color@6.1.0) + esbuild: 0.18.20 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss-load-config: 4.0.1(postcss@8.4.23) + resolve-from: 5.0.0 + rollup: 3.28.1 + source-map: 0.8.0-beta.0 + sucrase: 3.32.0 + tree-kill: 1.2.2 + transitivePeerDependencies: + - supports-color + - ts-node dev: true - /tsutils@3.21.0(typescript@5.0.4): + /tsutils@3.21.0(typescript@5.2.2): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 5.0.4 + typescript: 5.2.2 dev: true /tty-browserify@0.0.0: @@ -24056,6 +23126,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] @@ -24189,11 +23265,6 @@ packages: engines: {node: '>=10'} dev: false - /type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} - dev: true - /type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -24235,12 +23306,6 @@ packages: for-each: 0.3.3 is-typed-array: 1.1.10 - /typedarray-to-buffer@3.1.5: - resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} - dependencies: - is-typedarray: 1.0.0 - dev: true - /typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} @@ -24250,9 +23315,9 @@ packages: hasBin: true dev: true - /typescript@5.0.4: - resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} - engines: {node: '>=12.20'} + /typescript@5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + engines: {node: '>=14.17'} hasBin: true /ua-parser-js@0.7.35: @@ -24556,15 +23621,6 @@ packages: engines: {node: '>=8'} dev: true - /upath2@3.1.19: - resolution: {integrity: sha512-d23dQLi8nDWSRTIQwXtaYqMrHuca0As53fNiTLLFDmsGBbepsZepISaB2H1x45bDFN/n3Qw9bydvyZEacTrEWQ==} - dependencies: - '@types/node': 17.0.23 - path-is-network-drive: 1.0.20 - path-strip-sep: 1.0.17 - tslib: 2.5.0 - dev: true - /upath@1.2.0: resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} engines: {node: '>=4'} @@ -24746,23 +23802,10 @@ packages: kleur: 4.1.5 sade: 1.8.1 - /v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - dev: true - /v8-compile-cache@2.3.0: resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} dev: true - /v8-to-istanbul@8.1.1: - resolution: {integrity: sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==} - engines: {node: '>=10.12.0'} - dependencies: - '@types/istanbul-lib-coverage': 2.0.4 - convert-source-map: 1.9.0 - source-map: 0.7.4 - dev: true - /v8-to-istanbul@9.1.0: resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} engines: {node: '>=10.12.0'} @@ -24872,7 +23915,7 @@ packages: picocolors: 1.0.0 source-map: 0.6.1 source-map-support: 0.5.21 - vite: 4.3.4(@types/node@17.0.23) + vite: 4.3.4(@types/node@20.1.2) transitivePeerDependencies: - '@types/node' - less @@ -24883,7 +23926,7 @@ packages: - terser dev: true - /vite-node@0.32.2(@types/node@17.0.23): + /vite-node@0.32.2(@types/node@20.1.2): resolution: {integrity: sha512-dTQ1DCLwl2aEseov7cfQ+kDMNJpM1ebpyMMMwWzBvLbis8Nla/6c9WQcqpPssTwS6Rp/+U6KwlIj8Eapw4bLdA==} engines: {node: '>=v14.18.0'} hasBin: true @@ -24893,7 +23936,7 @@ packages: mlly: 1.2.0 pathe: 1.1.0 picocolors: 1.0.0 - vite: 4.3.4(@types/node@17.0.23) + vite: 4.3.4(@types/node@20.1.2) transitivePeerDependencies: - '@types/node' - less @@ -24904,7 +23947,7 @@ packages: - terser dev: true - /vite@4.3.4(@types/node@17.0.23): + /vite@4.3.4(@types/node@20.1.2): resolution: {integrity: sha512-f90aqGBoxSFxWph2b39ae2uHAxm5jFBBdnfueNxZAT1FTpM13ccFQExCaKbR2xFW5atowjleRniQ7onjJ22QEg==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -24929,9 +23972,9 @@ packages: terser: optional: true dependencies: - '@types/node': 17.0.23 + '@types/node': 20.1.2 esbuild: 0.17.19 - postcss: 8.4.24 + postcss: 8.4.31 rollup: 3.28.1 optionalDependencies: fsevents: 2.3.2 @@ -24971,7 +24014,7 @@ packages: '@edge-runtime/vm': 3.1.3 '@types/chai': 4.3.5 '@types/chai-subset': 1.3.3 - '@types/node': 17.0.23 + '@types/node': 20.1.2 '@vitest/expect': 0.32.2 '@vitest/runner': 0.32.2 '@vitest/snapshot': 0.32.2 @@ -24991,8 +24034,8 @@ packages: strip-literal: 1.0.1 tinybench: 2.5.0 tinypool: 0.5.0 - vite: 4.3.4(@types/node@17.0.23) - vite-node: 0.32.2(@types/node@17.0.23) + vite: 4.3.4(@types/node@20.1.2) + vite-node: 0.32.2(@types/node@20.1.2) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -25028,20 +24071,6 @@ packages: resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} dev: false - /w3c-hr-time@1.0.2: - resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} - deprecated: Use your platform's native performance.now() and performance.timeOrigin. - dependencies: - browser-process-hrtime: 1.0.0 - dev: true - - /w3c-xmlserializer@2.0.0: - resolution: {integrity: sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==} - engines: {node: '>=10'} - dependencies: - xml-name-validator: 3.0.0 - dev: true - /w3c-xmlserializer@4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} @@ -25120,14 +24149,8 @@ packages: /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - /webidl-conversions@5.0.0: - resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} - engines: {node: '>=8'} - dev: true - - /webidl-conversions@6.1.0: - resolution: {integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==} - engines: {node: '>=10.4'} + /webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true /webidl-conversions@7.0.0: @@ -25272,7 +24295,7 @@ packages: - supports-color dev: false - /webpack@5.88.1: + /webpack@5.88.1(esbuild@0.18.20): resolution: {integrity: sha512-FROX3TxQnC/ox4N+3xQoWZzvGXSuscxR32rbzjpXgEzWudJFEJBpdlkkob2ylrv5yzzufD1zph1OoFsLtm6stQ==} engines: {node: '>=10.13.0'} hasBin: true @@ -25303,7 +24326,7 @@ packages: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.9(webpack@5.88.1) + terser-webpack-plugin: 5.3.9(esbuild@0.18.20)(webpack@5.88.1) watchpack: 2.4.0 webpack-sources: 3.2.3 transitivePeerDependencies: @@ -25338,12 +24361,6 @@ packages: engines: {node: '>=6'} dev: true - /whatwg-encoding@1.0.5: - resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} - dependencies: - iconv-lite: 0.4.24 - dev: true - /whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} @@ -25355,10 +24372,6 @@ packages: resolution: {integrity: sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==} dev: false - /whatwg-mimetype@2.3.0: - resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} - dev: true - /whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} @@ -25378,13 +24391,12 @@ packages: tr46: 0.0.3 webidl-conversions: 3.0.1 - /whatwg-url@8.7.0: - resolution: {integrity: sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==} - engines: {node: '>=10'} + /whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} dependencies: - lodash: 4.17.21 - tr46: 2.1.0 - webidl-conversions: 6.1.0 + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 dev: true /which-boxed-primitive@1.0.2: @@ -25425,7 +24437,6 @@ packages: /which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - dev: false /which-typed-array@1.1.11: resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} @@ -25499,14 +24510,6 @@ packages: microevent.ts: 0.1.1 dev: false - /wrap-ansi@3.0.1: - resolution: {integrity: sha512-iXR3tDXpbnTpzjKSylUJRkLuOrEC7hwEB221cgn6wtF8wpmz28puFXAEfPT5zrjM3wahygB//VuWEr1vTkDcNQ==} - engines: {node: '>=4'} - dependencies: - string-width: 2.1.1 - strip-ansi: 4.0.0 - dev: true - /wrap-ansi@5.1.0: resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==} engines: {node: '>=6'} @@ -25514,7 +24517,6 @@ packages: ansi-styles: 3.2.1 string-width: 3.1.0 strip-ansi: 5.2.0 - dev: false /wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} @@ -25552,15 +24554,6 @@ packages: imurmurhash: 0.1.4 signal-exit: 3.0.7 - /write-file-atomic@3.0.3: - resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} - dependencies: - imurmurhash: 0.1.4 - is-typedarray: 1.0.0 - signal-exit: 3.0.7 - typedarray-to-buffer: 3.1.5 - dev: true - /write-file-atomic@4.0.1: resolution: {integrity: sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16} @@ -25696,10 +24689,6 @@ packages: - supports-color dev: true - /xml-name-validator@3.0.0: - resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==} - dev: true - /xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} @@ -25742,7 +24731,6 @@ packages: /y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} - dev: false /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} @@ -25773,7 +24761,6 @@ packages: dependencies: camelcase: 5.3.1 decamelize: 1.2.0 - dev: false /yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} @@ -25811,7 +24798,6 @@ packages: which-module: 2.0.1 y18n: 4.0.3 yargs-parser: 13.1.2 - dev: false /yargs@15.4.1: resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} @@ -25863,11 +24849,6 @@ packages: fd-slicer: 1.1.0 dev: true - /yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} - dev: true - /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} diff --git a/scripts/getBuildConfig.js b/scripts/getBuildConfig.js new file mode 100644 index 000000000..693913e97 --- /dev/null +++ b/scripts/getBuildConfig.js @@ -0,0 +1,110 @@ +/* eslint-env node */ +const fs = require('fs'); +const {babel} = require('@rollup/plugin-babel'); +const commonjs = require('@rollup/plugin-commonjs'); +const resolve = require('@rollup/plugin-node-resolve'); +const replace = require('@rollup/plugin-replace'); +const terser = require('@rollup/plugin-terser'); +const execa = require('execa'); + +const extensions = [...resolve.DEFAULTS.extensions, '.tsx']; + +const outDir = 'dist/'; + +function writeEnvIndex(input) { + Object.keys(input).forEach((key) => { + fs.writeFileSync( + `./${outDir}${key}.js`, + `'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./production/${key}.js'); +} else { + module.exports = require('./development/${key}.js'); +} +` + ); + }); +} + +async function buildTypes() { + await execa( + 'tsc', + '--noEmit false --emitDeclarationOnly true --outDir dist/types'.split(' ') + ); + // eslint-disable-next-line no-console + console.log('\ncreated types'); +} + +module.exports = function getConfig({ + env, + external = [], + input, + output, + plugins = [], + ...rest +}) { + /** @type import('rollup').RollupOptions */ + const config = { + input, + external: [/node_modules/, ...external], + output: { + dir: outDir + env, + format: 'cjs', + interop: 'auto', + freeze: false, + esModule: true, + exports: 'named', + ...output, + }, + treeshake: { + moduleSideEffects: false, + preset: 'smallest', + propertyReadSideEffects: false + }, + plugins: [ + resolve({extensions}), + commonjs(), + babel({ + babelHelpers: 'bundled', + extensions, + presets: [ + '@babel/preset-typescript', + '@babel/preset-react', + [ + '@babel/preset-env', + { + targets: { + // Same as https://nextjs.org/docs/architecture/supported-browsers#browserslist + browsers: [ + "chrome 64", + "edge 79", + "firefox 67", + "opera 51", + "safari 12" + ] + } + } + ] + ] + }), + replace({ + 'process.env.NODE_ENV': JSON.stringify(env), + preventAssignment: true + }), + env !== 'development' && terser(), + { + buildEnd() { + if (env === 'production') { + writeEnvIndex(input); + buildTypes(); + } + } + }, + ...plugins + ], + ...rest + }; + + return config; +};