From bdbca1c9ad4751aa019b7d47682398753ef57b6a Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 21 Jul 2023 14:05:39 +0200 Subject: [PATCH 01/37] feat: Add built-in pathname localization --- packages/next-intl/package.json | 1 + .../src/middleware/LocalizedPathnames.tsx | 126 ++ .../middleware/NextIntlMiddlewareConfig.tsx | 59 +- .../getAlternateLinksHeaderValue.tsx | 13 +- .../next-intl/src/middleware/middleware.tsx | 139 ++- .../src/middleware/resolveLocale.tsx | 39 +- packages/next-intl/src/middleware/utils.tsx | 60 +- packages/next-intl/src/server/index.tsx | 4 +- .../getAlternateLinksHeaderValue.test.tsx | 8 +- .../test/middleware/middleware.test.tsx | 219 ++++ pnpm-lock.yaml | 1021 ++++++++++++----- 11 files changed, 1282 insertions(+), 407 deletions(-) create mode 100644 packages/next-intl/src/middleware/LocalizedPathnames.tsx diff --git a/packages/next-intl/package.json b/packages/next-intl/package.json index dda2fdc56..bfdd64810 100644 --- a/packages/next-intl/package.json +++ b/packages/next-intl/package.json @@ -67,6 +67,7 @@ "dependencies": { "@formatjs/intl-localematcher": "^0.2.32", "negotiator": "^0.6.3", + "path-to-regexp": "^6.2.1", "use-intl": "^2.19.0" }, "peerDependencies": { diff --git a/packages/next-intl/src/middleware/LocalizedPathnames.tsx b/packages/next-intl/src/middleware/LocalizedPathnames.tsx new file mode 100644 index 000000000..d058c9d3d --- /dev/null +++ b/packages/next-intl/src/middleware/LocalizedPathnames.tsx @@ -0,0 +1,126 @@ +import {NextRequest} from 'next/server'; +import { + AllLocales, + MiddlewareConfigWithDefaults +} from './NextIntlMiddlewareConfig'; +import { + formatPathname, + getKnownLocaleFromPathname, + getPathWithSearch, + getRouteParams, + matchesPathname +} from './utils'; + +export function getLocalizedRedirectPathname( + request: NextRequest, + resolvedLocale: Locales[number], + configWithDefaults: MiddlewareConfigWithDefaults +) { + if (!configWithDefaults.pathnames) return; + + const {pathname} = request.nextUrl; + const pathLocale = getKnownLocaleFromPathname( + request.nextUrl.pathname, + configWithDefaults.locales + ); + + if (pathLocale) { + if (pathLocale === configWithDefaults.defaultLocale) { + return; + } + + for (const [, routePath] of Object.entries(configWithDefaults.pathnames)) { + if (typeof routePath === 'string') { + // No redirect is necessary if all locales use the same pathname + continue; + } + + const defaultLocaleTemplate = routePath[configWithDefaults.defaultLocale]; + const pathLocalePathname = `/${pathLocale}${defaultLocaleTemplate}`; + const matches = matchesPathname(pathLocalePathname, pathname); + + if (matches) { + const params = getRouteParams(pathLocalePathname, pathname); + return getPathWithSearch( + `/${pathLocale}` + formatPathname(routePath[pathLocale], params), + request.nextUrl.search + ); + } + } + } else if (resolvedLocale !== configWithDefaults.defaultLocale) { + if (resolvedLocale === configWithDefaults.defaultLocale) { + return; + } + + // Check if the path matches a route from the default locale. + // If this is the case, then redirect to a localized version. + for (const [, routePath] of Object.entries(configWithDefaults.pathnames)) { + if (typeof routePath === 'string') { + // No redirect is necessary if all locales use the same pathname + continue; + } + + const defaultLocalePathname = routePath[configWithDefaults.defaultLocale]; + const matches = matchesPathname(defaultLocalePathname, pathname); + + if (matches) { + const params = getRouteParams(defaultLocalePathname, pathname); + return getPathWithSearch( + `/${resolvedLocale}` + + formatPathname(routePath[resolvedLocale], params), + request.nextUrl.search + ); + } + } + } + + return; +} + +/** + * Checks if the request matches a localized route + * and returns the rewritten pathname if so. + */ +export function getLocalizedRewritePathname( + request: NextRequest, + configWithDefaults: MiddlewareConfigWithDefaults +) { + if (!configWithDefaults.pathnames) return; + + const {pathname} = request.nextUrl; + const pathLocale = getKnownLocaleFromPathname( + request.nextUrl.pathname, + configWithDefaults.locales + ); + + if ( + // When using unprefixed routing, we assume that the + // pathname uses routes from the default locale + !pathLocale || + // Internal routes are set up based on the default locale + pathLocale === configWithDefaults.defaultLocale + ) { + return; + } + + for (const [, routePath] of Object.entries(configWithDefaults.pathnames)) { + if (typeof routePath === 'string') { + // No rewrite is necessary if all locales use the same pathname + continue; + } + + const defaultLocalePathname = routePath[configWithDefaults.defaultLocale]; + const pathLocalePathname = `/${pathLocale}${routePath[pathLocale]}`; + const matches = matchesPathname(pathLocalePathname, pathname); + + if (matches) { + const params = getRouteParams(pathLocalePathname, pathname); + return getPathWithSearch( + `/${pathLocale}` + formatPathname(defaultLocalePathname, params), + request.nextUrl.search + ); + } + } + + return; +} diff --git a/packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx b/packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx index 7c29d95be..62b96fb35 100644 --- a/packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx +++ b/packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx @@ -1,42 +1,57 @@ type LocalePrefix = 'as-needed' | 'always' | 'never'; -type RoutingBaseConfig = { +type Locale = string; +export type AllLocales = ReadonlyArray; + +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; -}; +export type Pathnames = Record< + string, + {[Key in Locales[number]]: string} | string +>; + +// TODO: Default or not? +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; + + /** TODO */ + pathnames?: Pathnames; + }; + +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 bb948ce95..25d5196d5 100644 --- a/packages/next-intl/src/middleware/getAlternateLinksHeaderValue.tsx +++ b/packages/next-intl/src/middleware/getAlternateLinksHeaderValue.tsx @@ -1,10 +1,14 @@ import {NextRequest} from 'next/server'; import MiddlewareConfig, { + AllLocales, MiddlewareConfigWithDefaults } from './NextIntlMiddlewareConfig'; import {isLocaleSupportedOnDomain} from './utils'; -function getUnprefixedUrl(config: MiddlewareConfig, request: NextRequest) { +function getUnprefixedUrl( + config: MiddlewareConfig, + request: NextRequest +) { const url = new URL(request.url); if (!url.pathname.endsWith('/')) { url.pathname += '/'; @@ -30,10 +34,9 @@ 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 -) { +export default function getAlternateLinksHeaderValue< + Locales extends AllLocales +>(config: MiddlewareConfigWithDefaults, request: NextRequest) { const unprefixedUrl = getUnprefixedUrl(config, request); const links = config.locales.flatMap((locale) => { diff --git a/packages/next-intl/src/middleware/middleware.tsx b/packages/next-intl/src/middleware/middleware.tsx index bb0bfbf2e..444256334 100644 --- a/packages/next-intl/src/middleware/middleware.tsx +++ b/packages/next-intl/src/middleware/middleware.tsx @@ -1,34 +1,42 @@ import {NextRequest, NextResponse} from 'next/server'; import {COOKIE_LOCALE_NAME} from '../shared/constants'; import MiddlewareConfig, { + AllLocales, MiddlewareConfigWithDefaults } from './NextIntlMiddlewareConfig'; import getAlternateLinksHeaderValue from './getAlternateLinksHeaderValue'; +import {getLocalizedRewritePathname,getLocalizedRedirectPathname} from './LocalizedPathnames'; import resolveLocale from './resolveLocale'; import { + getBasePath, getBestMatchingDomain, - getLocaleFromPathname, + getKnownLocaleFromPathname, + getPathWithSearch, isLocaleSupportedOnDomain } from './utils'; const ROOT_URL = '/'; -function receiveConfig(config: MiddlewareConfig) { - const result: MiddlewareConfigWithDefaults = { +function receiveConfig( + config: MiddlewareConfig +): MiddlewareConfigWithDefaults { + return { ...config, alternateLinks: config.alternateLinks ?? true, localePrefix: config.localePrefix ?? 'as-needed', localeDetection: config.localeDetection ?? true }; - - return result; } -export default function createMiddleware(config: MiddlewareConfig) { +// TODO: eslint-config-molindo needs an upgrade of @typescript-eslint/parser +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. + // TODO: Remove in next major release. const matcher: Array | undefined = (config as any)._matcher; return function middleware(request: NextRequest) { @@ -107,11 +115,12 @@ export default function createMiddleware(config: MiddlewareConfig) { } let response; + if (isRoot) { - let pathWithSearch = `/${locale}`; - if (request.nextUrl.search) { - pathWithSearch += request.nextUrl.search; - } + const pathWithSearch = getPathWithSearch( + `/${locale}`, + request.nextUrl.search + ); if ( configWithDefaults.localePrefix === 'never' || @@ -123,62 +132,78 @@ export default function createMiddleware(config: MiddlewareConfig) { 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; + if (configWithDefaults.pathnames) { + const localizedRedirect = getLocalizedRedirectPathname( + request, + locale, + configWithDefaults + ) + if (localizedRedirect) { + response = redirect(localizedRedirect); + } else { + const localizedRewrite = getLocalizedRewritePathname( + request, + configWithDefaults + ); + if (localizedRewrite) { + response = rewrite(localizedRewrite); + } + } } - if (hasLocalePrefix) { - const basePath = pathWithSearch.replace(`/${pathLocale}`, '') || '/'; - - if (configWithDefaults.localePrefix === 'never') { - response = redirect(basePath); - } else if (pathLocale === locale) { - if ( - hasMatchedDefaultLocale && - configWithDefaults.localePrefix === 'as-needed' - ) { + if (!response) { + const pathLocale = getKnownLocaleFromPathname( + request.nextUrl.pathname, + configWithDefaults.locales + ); + const hasLocalePrefix = pathLocale != null; + const pathWithSearch = getPathWithSearch( + request.nextUrl.pathname, + request.nextUrl.search + ); + + if (hasLocalePrefix) { + const basePath = getBasePath(pathWithSearch, pathLocale); + + 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 = next(); + } } else { response = next(); } - } 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}`); + } } } } diff --git a/packages/next-intl/src/middleware/resolveLocale.tsx b/packages/next-intl/src/middleware/resolveLocale.tsx index 2698f2443..aee785928 100644 --- a/packages/next-intl/src/middleware/resolveLocale.tsx +++ b/packages/next-intl/src/middleware/resolveLocale.tsx @@ -3,6 +3,7 @@ import Negotiator from 'negotiator'; import {RequestCookies} from 'next/dist/server/web/spec-extension/cookies'; import {COOKIE_LOCALE_NAME} from '../shared/constants'; import { + AllLocales, DomainConfig, MiddlewareConfigWithDefaults } from './NextIntlMiddlewareConfig'; @@ -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: string; 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..a8ffa0afe 100644 --- a/packages/next-intl/src/middleware/utils.tsx +++ b/packages/next-intl/src/middleware/utils.tsx @@ -1,9 +1,57 @@ -import {DomainConfig} from './NextIntlMiddlewareConfig'; +import {pathToRegexp, match, compile} from 'path-to-regexp'; +import {AllLocales, DomainConfig} from './NextIntlMiddlewareConfig'; export function getLocaleFromPathname(pathname: string) { return pathname.split('/')[1]; } +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) { + return pathname.replace(`/${pathLocale}`, '') || '/'; +} + +export function matchesPathname(template: string, pathname: string) { + const regex = pathToRegexp(template); + const matches = regex.exec(pathname); + return matches != null; +} + +export function getRouteParams(template: string, pathname: string) { + const fn = match( + template + // { decode: decodeURIComponent } + ); + + const result = fn(pathname); + return result ? result.params : undefined; +} + +export function formatPathname(template: string, params?: object) { + const toPath = compile(template); + return toPath(params); +} + +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 +60,9 @@ export function getHost(requestHeaders: Headers) { ); } -export function isLocaleSupportedOnDomain( +export function isLocaleSupportedOnDomain( locale: string, - domain: DomainConfig + domain: DomainConfig ) { return ( domain.defaultLocale === locale || @@ -23,10 +71,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/server/index.tsx b/packages/next-intl/src/server/index.tsx index f04ae0554..73add9855 100644 --- a/packages/next-intl/src/server/index.tsx +++ b/packages/next-intl/src/server/index.tsx @@ -3,11 +3,11 @@ */ import createMiddleware_ from '../middleware'; -import MiddlewareConfig from '../middleware/NextIntlMiddlewareConfig'; +import MiddlewareConfig, { AllLocales } 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) { +export function createIntlMiddleware(config: MiddlewareConfig) { if (!hasWarned) { hasWarned = true; console.warn( diff --git a/packages/next-intl/test/middleware/getAlternateLinksHeaderValue.test.tsx b/packages/next-intl/test/middleware/getAlternateLinksHeaderValue.test.tsx index cb3fc3406..bcea933c1 100644 --- a/packages/next-intl/test/middleware/getAlternateLinksHeaderValue.test.tsx +++ b/packages/next-intl/test/middleware/getAlternateLinksHeaderValue.test.tsx @@ -8,7 +8,7 @@ function getRequest(url = 'https://example.com/') { } it('works for prefixed routing (as-needed)', () => { - const config: MiddlewareConfigWithDefaults = { + const config: MiddlewareConfigWithDefaults<['en', 'es']> = { defaultLocale: 'en', locales: ['en', 'es'], alternateLinks: true, @@ -37,7 +37,7 @@ it('works for prefixed routing (as-needed)', () => { }); it('works for prefixed routing (always)', () => { - const config: MiddlewareConfigWithDefaults = { + const config: MiddlewareConfigWithDefaults<['en', 'es']> = { defaultLocale: 'en', locales: ['en', 'es'], alternateLinks: true, @@ -66,7 +66,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, @@ -124,7 +124,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, diff --git a/packages/next-intl/test/middleware/middleware.test.tsx b/packages/next-intl/test/middleware/middleware.test.tsx index 30ec000ac..ddef7cdea 100644 --- a/packages/next-intl/test/middleware/middleware.test.tsx +++ b/packages/next-intl/test/middleware/middleware.test.tsx @@ -219,6 +219,138 @@ describe('prefix-based routing', () => { ].join(', ') ); }); + + describe('localized pathnames', () => { + const middlewareWithPathnames = createIntlMiddleware({ + defaultLocale: 'en', + locales: ['en', 'de'], + pathnames: { + home: '/', + about: { + en: '/about', + de: '/über' + }, + users: { + en: '/users', + de: '/benutzer' + }, + 'users-detail': { + en: '/users/:userId', + de: '/benutzer/:userId' + }, + 'news-detail': { + en: '/news/:articleSlug-:articleId', + de: '/neuigkeiten/:articleSlug-:articleId' + } + } + }); + + 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') + ); + + 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/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' + ); + }); + + it('serves requests for a non-default locale at the root', () => { + middlewareWithPathnames(createMockRequest('/de', 'de')); + expect(MockedNextResponse.next).toHaveBeenCalled(); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + }); + + it('serves requests for a non-default locale at nested paths', () => { + middlewareWithPathnames(createMockRequest('/de/über', 'de')); + middlewareWithPathnames(createMockRequest('/de/benutzer', 'de')); + middlewareWithPathnames(createMockRequest('/de/benutzer/1', 'de')); + middlewareWithPathnames( + createMockRequest('/de/neuigkeiten/happy-newyear-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/happy-newyear-g5b116754' + ); + }); + + it('forwards a request for a localized route that is not associated with the requested locale so that a 404 response can be returned', () => { + middlewareWithPathnames(createMockRequest('/über', 'en')); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).toHaveBeenCalledTimes(1); + expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/en/%C3%BCber' + ); + }); + + 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')); + 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/%C3%BCber' + ); + expect(MockedNextResponse.redirect.mock.calls[1][0].toString()).toBe( + 'http://localhost:3000/de/benutzer/2' + ); + }); + + 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/%C3%BCber' + ); + expect(MockedNextResponse.redirect.mock.calls[1][0].toString()).toBe( + 'http://localhost:3000/de/benutzer/2' + ); + }); + + // domain routing tests + }); }); describe('localePrefix: as-needed, localeDetection: false', () => { @@ -306,6 +438,93 @@ describe('prefix-based routing', () => { expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); expect(MockedNextResponse.next).toHaveBeenCalled(); }); + + describe('localized pathnames', () => { + const middlewareWithPathnames = createIntlMiddleware({ + defaultLocale: 'en', + locales: ['en', 'de'], + localePrefix: 'always', + pathnames: { + home: '/', + about: { + en: '/about', + de: '/über' + }, + users: { + en: '/users', + de: '/benutzer' + }, + 'users-detail': { + en: '/users/:userId', + de: '/benutzer/:userId' + }, + 'news-detail': { + en: '/news/:articleSlug-:articleId', + de: '/neuigkeiten/:articleSlug-:articleId' + } + } + }); + + it('serves requests for the default locale at the root', () => { + middlewareWithPathnames(createMockRequest('/en', 'en')); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).toHaveBeenCalled(); + }); + + 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.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).toHaveBeenCalledTimes(4); + }); + + it('serves requests for a non-default locale at the root', () => { + middlewareWithPathnames(createMockRequest('/de', 'de')); + expect(MockedNextResponse.next).toHaveBeenCalled(); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + }); + + it('serves requests for a non-default locale at nested paths', () => { + middlewareWithPathnames(createMockRequest('/de/über', '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('forwards a request for a localized route that is not associated with the requested locale so that a 404 response can be returned', () => { + // Relevant to avoid duplicate content issues + middlewareWithPathnames(createMockRequest('/en/über', 'en')); + middlewareWithPathnames(createMockRequest('/en/benutzer/12', 'en')); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); + expect(MockedNextResponse.next).toHaveBeenCalledTimes(2); + }); + }); }); describe('localePrefix: never', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51fc18af4..48e361ec0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -34,7 +34,7 @@ importers: version: 2.2.0 next: specifier: ^13.4.7 - version: 13.4.7(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + version: 13.4.7(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) nextra: specifier: ^2.8.0 version: 2.8.0(next@13.4.7)(react-dom@18.2.0)(react@18.2.0) @@ -49,7 +49,7 @@ importers: version: 18.2.0(react@18.2.0) tailwindcss: specifier: ^3.2.4 - version: 3.2.4(postcss@8.4.24) + version: 3.2.4(postcss@8.4.26) devDependencies: '@types/node': specifier: 20.1.2 @@ -59,7 +59,7 @@ importers: version: 18.2.5 autoprefixer: specifier: ^10.4.0 - version: 10.4.0(postcss@8.4.24) + version: 10.4.0(postcss@8.4.26) eslint: specifier: ^8.39.0 version: 8.39.0 @@ -71,10 +71,10 @@ importers: version: 13.4.0(eslint@8.39.0)(typescript@5.0.4) next-sitemap: specifier: ^4.0.7 - version: 4.0.7(@next/env@13.4.7)(next@13.4.7) + version: 4.0.7(@next/env@13.4.11)(next@13.4.7) prettier-plugin-tailwindcss: specifier: ^0.2.3 - version: 0.2.3(prettier@2.8.8) + version: 0.2.3(prettier@3.0.0) typescript: specifier: ^5.0.0 version: 5.0.4 @@ -86,7 +86,7 @@ importers: version: 2.16.1 next: specifier: ^13.4.7 - version: 13.4.7(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + version: 13.4.7(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) next-intl: specifier: ^2.14.3 version: link:../../packages/next-intl @@ -129,7 +129,7 @@ importers: version: 4.17.21 next: specifier: ^13.4.7 - version: 13.4.7(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + version: 13.4.7(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) next-intl: specifier: ^2.14.3 version: link:../../packages/next-intl @@ -184,7 +184,7 @@ importers: version: 1.2.1 next: specifier: ^13.4.7 - version: 13.4.7(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + version: 13.4.7(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) next-intl: specifier: ^2.14.3 version: link:../../packages/next-intl @@ -242,7 +242,7 @@ importers: version: 8.4.23 prettier-plugin-tailwindcss: specifier: ^0.2.3 - version: 0.2.3(prettier@2.8.8) + version: 0.2.3(prettier@3.0.0) typescript: specifier: ^5.0.0 version: 5.0.4 @@ -251,7 +251,7 @@ importers: dependencies: next: specifier: ^13.4.7 - version: 13.4.7(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + version: 13.4.7(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) next-auth: specifier: ^4.22.1 version: 4.22.1(next@13.4.7)(react-dom@18.2.0)(react@18.2.0) @@ -294,7 +294,7 @@ importers: 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.1.6) expo: specifier: ~47.0.12 version: 47.0.12(@babel/core@7.21.8) @@ -309,7 +309,7 @@ importers: version: 18.1.0(react@18.1.0) react-native: specifier: ^0.70.5 - version: 0.70.5(@babel/core@7.21.8)(@babel/preset-env@7.22.5)(react@18.1.0) + version: 0.70.5(@babel/core@7.21.8)(@babel/preset-env@7.22.9)(react@18.1.0) react-native-web: specifier: ~0.18.9 version: 0.18.9(react-dom@18.1.0)(react@18.1.0) @@ -375,6 +375,9 @@ importers: negotiator: specifier: ^0.6.3 version: 0.6.3 + path-to-regexp: + specifier: ^6.2.1 + version: 6.2.1 use-intl: specifier: ^2.19.0 version: link:../use-intl @@ -396,7 +399,7 @@ importers: 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.21.5)(@types/node@17.0.23) + version: 1.4.0(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.5)(@types/node@17.0.23) eslint: specifier: ^8.39.0 version: 8.39.0 @@ -408,7 +411,7 @@ importers: version: 1.4.1(eslint@8.39.0)(typescript@5.0.4) next: specifier: ^13.4.7 - version: 13.4.7(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + version: 13.4.7(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) react: specifier: ^18.2.0 version: 18.2.0 @@ -451,7 +454,7 @@ importers: 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.21.5)(@types/node@17.0.23) + version: 1.4.0(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.5)(@types/node@17.0.23) eslint: specifier: ^8.39.0 version: 8.39.0 @@ -509,6 +512,10 @@ packages: resolution: {integrity: sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==} engines: {node: '>=6.9.0'} + /@babel/compat-data@7.22.9: + resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==} + engines: {node: '>=6.9.0'} + /@babel/core@7.21.8: resolution: {integrity: sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ==} engines: {node: '>=6.9.0'} @@ -553,6 +560,28 @@ packages: transitivePeerDependencies: - supports-color + /@babel/core@7.22.9: + resolution: {integrity: sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.22.5 + '@babel/generator': 7.22.9 + '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.22.9) + '@babel/helper-module-transforms': 7.22.9(@babel/core@7.22.9) + '@babel/helpers': 7.22.6 + '@babel/parser': 7.22.7 + '@babel/template': 7.22.5 + '@babel/traverse': 7.22.8 + '@babel/types': 7.22.5 + convert-source-map: 1.9.0 + debug: 4.3.4(supports-color@6.1.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + /@babel/core@7.9.0: resolution: {integrity: sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==} engines: {node: '>=6.9.0'} @@ -571,7 +600,7 @@ packages: json5: 2.2.3 lodash: 4.17.21 resolve: 1.22.2 - semver: 5.7.1 + semver: 5.7.2 source-map: 0.5.7 transitivePeerDependencies: - supports-color @@ -595,6 +624,15 @@ packages: '@jridgewell/trace-mapping': 0.3.18 jsesc: 2.5.2 + /@babel/generator@7.22.9: + resolution: {integrity: sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.5 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.18 + jsesc: 2.5.2 + /@babel/helper-annotate-as-pure@7.22.5: resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} engines: {node: '>=6.9.0'} @@ -631,7 +669,7 @@ packages: '@babel/helper-validator-option': 7.22.5 browserslist: 4.21.9 lru-cache: 5.1.1 - semver: 6.3.0 + semver: 6.3.1 /@babel/helper-compilation-targets@7.22.5(@babel/core@7.22.5): resolution: {integrity: sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==} @@ -646,6 +684,33 @@ packages: lru-cache: 5.1.1 semver: 6.3.0 + /@babel/helper-compilation-targets@7.22.9(@babel/core@7.21.8): + resolution: {integrity: sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/compat-data': 7.22.9 + '@babel/core': 7.21.8 + '@babel/helper-validator-option': 7.22.5 + browserslist: 4.21.9 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: false + + /@babel/helper-compilation-targets@7.22.9(@babel/core@7.22.9): + resolution: {integrity: sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/compat-data': 7.22.9 + '@babel/core': 7.22.9 + '@babel/helper-validator-option': 7.22.5 + browserslist: 4.21.9 + lru-cache: 5.1.1 + semver: 6.3.1 + /@babel/helper-create-class-features-plugin@7.22.5(@babel/core@7.21.8): resolution: {integrity: sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==} engines: {node: '>=6.9.0'} @@ -661,7 +726,7 @@ packages: '@babel/helper-replace-supers': 7.22.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 '@babel/helper-split-export-declaration': 7.22.5 - semver: 6.3.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -693,7 +758,7 @@ packages: '@babel/core': 7.21.8 '@babel/helper-annotate-as-pure': 7.22.5 regexpu-core: 5.3.2 - semver: 6.3.0 + semver: 6.3.1 /@babel/helper-create-regexp-features-plugin@7.22.5(@babel/core@7.22.5): resolution: {integrity: sha512-1VpEFOIbMRaXyDeUwUfmTIxExLwQ+zkW+Bh5zXpApA3oQedBx9v/updixWxnx/bZpKw7u8VxWjb/qWpIcmPq8A==} @@ -736,13 +801,13 @@ packages: transitivePeerDependencies: - supports-color - /@babel/helper-define-polyfill-provider@0.4.0(@babel/core@7.21.8): + /@babel/helper-define-polyfill-provider@0.4.0(@babel/core@7.22.5): resolution: {integrity: sha512-RnanLx5ETe6aybRi1cO/edaRH+bNYWaryCEmjDDYyNr4wnSzyOp8T0dWipmqVHKEY3AbVKUom50AKSlj1zmKbg==} peerDependencies: '@babel/core': ^7.4.0-0 dependencies: - '@babel/core': 7.21.8 - '@babel/helper-compilation-targets': 7.22.5(@babel/core@7.21.8) + '@babel/core': 7.22.5 + '@babel/helper-compilation-targets': 7.22.5(@babel/core@7.22.5) '@babel/helper-plugin-utils': 7.22.5 debug: 4.3.4(supports-color@6.1.0) lodash.debounce: 4.0.8 @@ -750,23 +815,22 @@ packages: semver: 6.3.0 transitivePeerDependencies: - supports-color - dev: false + dev: true - /@babel/helper-define-polyfill-provider@0.4.0(@babel/core@7.22.5): - resolution: {integrity: sha512-RnanLx5ETe6aybRi1cO/edaRH+bNYWaryCEmjDDYyNr4wnSzyOp8T0dWipmqVHKEY3AbVKUom50AKSlj1zmKbg==} + /@babel/helper-define-polyfill-provider@0.4.1(@babel/core@7.21.8): + resolution: {integrity: sha512-kX4oXixDxG197yhX+J3Wp+NpL2wuCFjWQAr6yX2jtCnflK9ulMI51ULFGIrWiX1jGfvAxdHp+XQCcP2bZGPs9A==} peerDependencies: '@babel/core': ^7.4.0-0 dependencies: - '@babel/core': 7.22.5 - '@babel/helper-compilation-targets': 7.22.5(@babel/core@7.22.5) + '@babel/core': 7.21.8 + '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.21.8) '@babel/helper-plugin-utils': 7.22.5 debug: 4.3.4(supports-color@6.1.0) lodash.debounce: 4.0.8 resolve: 1.22.2 - semver: 6.3.0 transitivePeerDependencies: - supports-color - dev: true + dev: false /@babel/helper-environment-visitor@7.21.5: resolution: {integrity: sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==} @@ -850,6 +914,19 @@ packages: transitivePeerDependencies: - supports-color + /@babel/helper-module-transforms@7.22.9(@babel/core@7.22.9): + resolution: {integrity: sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-module-imports': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.5 + /@babel/helper-optimise-call-expression@7.22.5: resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} engines: {node: '>=6.9.0'} @@ -888,6 +965,18 @@ packages: transitivePeerDependencies: - supports-color + /@babel/helper-remap-async-to-generator@7.22.9(@babel/core@7.21.8): + resolution: {integrity: sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-wrap-function': 7.22.9 + dev: false + /@babel/helper-replace-supers@7.22.5: resolution: {integrity: sha512-aLdNM5I3kdI/V9xGNyKSF3X/gTyMUBohTZ+/3QdQKAA9vxIiy12E+8E2HoOP1/DjeqU+g6as35QHJNMDDYpuCg==} engines: {node: '>=6.9.0'} @@ -901,6 +990,18 @@ packages: transitivePeerDependencies: - supports-color + /@babel/helper-replace-supers@7.22.9(@babel/core@7.21.8): + resolution: {integrity: sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-member-expression-to-functions': 7.22.5 + '@babel/helper-optimise-call-expression': 7.22.5 + dev: false + /@babel/helper-simple-access@7.21.5: resolution: {integrity: sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==} engines: {node: '>=6.9.0'} @@ -931,6 +1032,12 @@ packages: dependencies: '@babel/types': 7.22.5 + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.5 + /@babel/helper-string-parser@7.21.5: resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==} engines: {node: '>=6.9.0'} @@ -966,6 +1073,15 @@ packages: transitivePeerDependencies: - supports-color + /@babel/helper-wrap-function@7.22.9: + resolution: {integrity: sha512-sZ+QzfauuUEfxSEjKFmi3qDSHgLsTPK/pEpoD/qonZKOtTPTLbf59oabPQ4rKekt9lFcj/hTZaOhWwFYrgjk+Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-function-name': 7.22.5 + '@babel/template': 7.22.5 + '@babel/types': 7.22.5 + dev: false + /@babel/helpers@7.21.5: resolution: {integrity: sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==} engines: {node: '>=6.9.0'} @@ -986,6 +1102,16 @@ packages: transitivePeerDependencies: - supports-color + /@babel/helpers@7.22.6: + resolution: {integrity: sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.5 + '@babel/traverse': 7.22.8 + '@babel/types': 7.22.5 + transitivePeerDependencies: + - supports-color + /@babel/highlight@7.18.6: resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} engines: {node: '>=6.9.0'} @@ -1016,6 +1142,13 @@ packages: dependencies: '@babel/types': 7.22.5 + /@babel/parser@7.22.7: + resolution: {integrity: sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.22.5 + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.5(@babel/core@7.21.8): resolution: {integrity: sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==} engines: {node: '>=6.9.0'} @@ -1519,6 +1652,17 @@ packages: dependencies: '@babel/core': 7.22.5 '@babel/helper-plugin-utils': 7.22.5 + dev: false + + /@babel/plugin-syntax-flow@7.22.5(@babel/core@7.22.9): + resolution: {integrity: sha512-9RdCl0i+q0QExayk2nOS7853w08yLucnnPML6EN9S8fgMPVtdLDCdx/cOQ/i44Lb9UeQX9A35yaqBBOMMZxPxQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true /@babel/plugin-syntax-import-assertions@7.22.5(@babel/core@7.21.8): resolution: {integrity: sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==} @@ -1611,6 +1755,16 @@ packages: '@babel/core': 7.22.5 '@babel/helper-plugin-utils': 7.22.5 + /@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.22.9): + resolution: {integrity: sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.21.8): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: @@ -1805,35 +1959,33 @@ packages: '@babel/core': 7.22.5 '@babel/helper-plugin-utils': 7.22.5 - /@babel/plugin-transform-async-generator-functions@7.22.5(@babel/core@7.21.8): + /@babel/plugin-transform-async-generator-functions@7.22.5(@babel/core@7.22.5): resolution: {integrity: sha512-gGOEvFzm3fWoyD5uZq7vVTD57pPJ3PczPUD/xCFGjzBpUosnklmXyKnGQbbbGs1NPNPskFex0j93yKbHt0cHyg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.8 + '@babel/core': 7.22.5 '@babel/helper-environment-visitor': 7.22.5 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-remap-async-to-generator': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.8) + '@babel/helper-remap-async-to-generator': 7.22.5(@babel/core@7.22.5) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.5) transitivePeerDependencies: - supports-color - dev: false + dev: true - /@babel/plugin-transform-async-generator-functions@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-gGOEvFzm3fWoyD5uZq7vVTD57pPJ3PczPUD/xCFGjzBpUosnklmXyKnGQbbbGs1NPNPskFex0j93yKbHt0cHyg==} + /@babel/plugin-transform-async-generator-functions@7.22.7(@babel/core@7.21.8): + resolution: {integrity: sha512-7HmE7pk/Fmke45TODvxvkxRMV9RazV+ZZzhOL9AG8G29TLrr3jkjwF7uJfxZ30EoXpO+LJkq4oA8NjO2DTnEDg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.5 + '@babel/core': 7.21.8 '@babel/helper-environment-visitor': 7.22.5 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-remap-async-to-generator': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.5) - transitivePeerDependencies: - - supports-color - dev: true + '@babel/helper-remap-async-to-generator': 7.22.9(@babel/core@7.21.8) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.8) + dev: false /@babel/plugin-transform-async-to-generator@7.22.5(@babel/core@7.21.8): resolution: {integrity: sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==} @@ -1989,6 +2141,24 @@ packages: transitivePeerDependencies: - supports-color + /@babel/plugin-transform-classes@7.22.6(@babel/core@7.21.8): + resolution: {integrity: sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.21.8) + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-function-name': 7.22.5 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.9(@babel/core@7.21.8) + '@babel/helper-split-export-declaration': 7.22.6 + globals: 11.12.0 + dev: false + /@babel/plugin-transform-computed-properties@7.22.5(@babel/core@7.21.8): resolution: {integrity: sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==} engines: {node: '>=6.9.0'} @@ -2558,6 +2728,18 @@ packages: '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.22.5) dev: true + /@babel/plugin-transform-optional-chaining@7.22.6(@babel/core@7.21.8): + resolution: {integrity: sha512-Vd5HiWml0mDVtcLHIoEU5sw6HOUW/Zk0acLs/SAeuLzkGNOPc9DB4nkUajemhCmTIz3eiaKREZn2hQQqF79YTg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.8) + dev: false + /@babel/plugin-transform-parameters@7.22.5(@babel/core@7.21.8): resolution: {integrity: sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==} engines: {node: '>=6.9.0'} @@ -2736,6 +2918,21 @@ packages: '@babel/helper-plugin-utils': 7.22.5 '@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.22.5) '@babel/types': 7.22.5 + dev: false + + /@babel/plugin-transform-react-jsx@7.22.5(@babel/core@7.22.9): + resolution: {integrity: sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-module-imports': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.22.9) + '@babel/types': 7.22.5 + dev: true /@babel/plugin-transform-regenerator@7.22.5(@babel/core@7.21.8): resolution: {integrity: sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==} @@ -2789,7 +2986,7 @@ packages: babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.21.8) babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.21.8) babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.21.8) - semver: 6.3.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color dev: false @@ -2806,7 +3003,7 @@ packages: babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.22.5) babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.22.5) babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.22.5) - semver: 6.3.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color dev: false @@ -3098,101 +3295,10 @@ packages: babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.21.8) babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.21.8) core-js-compat: 3.31.0 - semver: 6.3.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color - /@babel/preset-env@7.22.5(@babel/core@7.21.8): - resolution: {integrity: sha512-fj06hw89dpiZzGZtxn+QybifF07nNiZjZ7sazs2aVDcysAZVGjW7+7iFYxg6GLNM47R/thYfLdrXc+2f11Vi9A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.22.5 - '@babel/core': 7.21.8 - '@babel/helper-compilation-targets': 7.22.5(@babel/core@7.21.8) - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-validator-option': 7.22.5 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.21.8) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.8) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.21.8) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.21.8) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.21.8) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.21.8) - '@babel/plugin-syntax-import-assertions': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-syntax-import-attributes': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.21.8) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.21.8) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.21.8) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.21.8) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.21.8) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.21.8) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.21.8) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.8) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.21.8) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.21.8) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.21.8) - '@babel/plugin-transform-arrow-functions': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-async-generator-functions': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-block-scoped-functions': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-block-scoping': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-class-properties': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-class-static-block': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-classes': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-computed-properties': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-destructuring': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-duplicate-keys': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-dynamic-import': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-exponentiation-operator': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-export-namespace-from': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-for-of': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-function-name': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-json-strings': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-literals': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-logical-assignment-operators': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-member-expression-literals': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-modules-amd': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-modules-commonjs': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-modules-systemjs': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-modules-umd': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-new-target': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-nullish-coalescing-operator': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-numeric-separator': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-object-rest-spread': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-object-super': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-optional-catch-binding': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-optional-chaining': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-parameters': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-private-methods': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-private-property-in-object': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-property-literals': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-regenerator': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-reserved-words': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-shorthand-properties': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-spread': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-sticky-regex': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-template-literals': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-typeof-symbol': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-unicode-escapes': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-unicode-property-regex': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-unicode-regex': 7.22.5(@babel/core@7.21.8) - '@babel/plugin-transform-unicode-sets-regex': 7.22.5(@babel/core@7.21.8) - '@babel/preset-modules': 0.1.5(@babel/core@7.21.8) - '@babel/types': 7.22.5 - babel-plugin-polyfill-corejs2: 0.4.3(@babel/core@7.21.8) - babel-plugin-polyfill-corejs3: 0.8.1(@babel/core@7.21.8) - babel-plugin-polyfill-regenerator: 0.5.0(@babel/core@7.21.8) - core-js-compat: 3.31.0 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: false - /@babel/preset-env@7.22.5(@babel/core@7.22.5): resolution: {integrity: sha512-fj06hw89dpiZzGZtxn+QybifF07nNiZjZ7sazs2aVDcysAZVGjW7+7iFYxg6GLNM47R/thYfLdrXc+2f11Vi9A==} engines: {node: '>=6.9.0'} @@ -3284,6 +3390,97 @@ packages: - supports-color dev: true + /@babel/preset-env@7.22.9(@babel/core@7.21.8): + resolution: {integrity: sha512-wNi5H/Emkhll/bqPjsjQorSykrlfY5OWakd6AulLvMEytpKasMVUpVy8RL4qBIBs5Ac6/5i0/Rv0b/Fg6Eag/g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.22.9 + '@babel/core': 7.21.8 + '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.21.8) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.5 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.21.8) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.8) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.21.8) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.21.8) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-import-assertions': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-syntax-import-attributes': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.21.8) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.21.8) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.21.8) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.21.8) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.21.8) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-transform-arrow-functions': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-async-generator-functions': 7.22.7(@babel/core@7.21.8) + '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-block-scoped-functions': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-block-scoping': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-class-properties': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-class-static-block': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-classes': 7.22.6(@babel/core@7.21.8) + '@babel/plugin-transform-computed-properties': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-destructuring': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-duplicate-keys': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-dynamic-import': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-exponentiation-operator': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-export-namespace-from': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-for-of': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-function-name': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-json-strings': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-literals': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-logical-assignment-operators': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-member-expression-literals': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-modules-amd': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-modules-commonjs': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-modules-systemjs': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-modules-umd': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-new-target': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-nullish-coalescing-operator': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-numeric-separator': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-object-rest-spread': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-object-super': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-optional-catch-binding': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-optional-chaining': 7.22.6(@babel/core@7.21.8) + '@babel/plugin-transform-parameters': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-private-methods': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-private-property-in-object': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-property-literals': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-regenerator': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-reserved-words': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-shorthand-properties': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-spread': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-sticky-regex': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-template-literals': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-typeof-symbol': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-unicode-escapes': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-unicode-property-regex': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-unicode-regex': 7.22.5(@babel/core@7.21.8) + '@babel/plugin-transform-unicode-sets-regex': 7.22.5(@babel/core@7.21.8) + '@babel/preset-modules': 0.1.5(@babel/core@7.21.8) + '@babel/types': 7.22.5 + babel-plugin-polyfill-corejs2: 0.4.4(@babel/core@7.21.8) + babel-plugin-polyfill-corejs3: 0.8.2(@babel/core@7.21.8) + babel-plugin-polyfill-regenerator: 0.5.1(@babel/core@7.21.8) + core-js-compat: 3.31.1 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/preset-flow@7.21.4(@babel/core@7.22.5): resolution: {integrity: sha512-F24cSq4DIBmhq4OzK3dE63NHagb27OPE3eWR+HLekt4Z3Y5MzIIUGF3LlLgV0gN8vzbDViSY7HnrReNVCJXTeA==} engines: {node: '>=6.9.0'} @@ -3426,6 +3623,23 @@ packages: transitivePeerDependencies: - supports-color + /@babel/traverse@7.22.8: + resolution: {integrity: sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.22.5 + '@babel/generator': 7.22.9 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-function-name': 7.22.5 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.22.7 + '@babel/types': 7.22.5 + debug: 4.3.4(supports-color@6.1.0) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + /@babel/types@7.21.5: resolution: {integrity: sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==} engines: {node: '>=6.9.0'} @@ -3972,7 +4186,7 @@ packages: qrcode-terminal: 0.11.0 requireg: 0.2.2 resolve-from: 5.0.0 - semver: 6.3.0 + semver: 6.3.1 send: 0.18.0(supports-color@6.1.0) slugify: 1.6.6 structured-headers: 0.4.1 @@ -4011,7 +4225,7 @@ packages: getenv: 1.0.0 glob: 7.1.6 resolve-from: 5.0.0 - semver: 7.5.0 + semver: 7.5.4 slash: 3.0.0 xcode: 3.0.1 xml2js: 0.4.23 @@ -4216,7 +4430,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.1.6): resolution: {integrity: sha512-cgcWyVXUEH5wj4InAPCIDHAGgpkQhpzWseCj4xVjdL3paBKRMWVjPUqmdHh/exap3U0kHGr/XS+e7ZWLcgHkUw==} engines: {node: '>=12'} dependencies: @@ -4238,9 +4452,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.1.6) 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.1.6)(webpack@4.43.0) schema-utils: 3.1.2 semver: 7.3.8 style-loader: 1.2.1(webpack@4.43.0) @@ -4878,7 +5092,7 @@ packages: p-reduce: 2.1.0 pacote: 15.1.1 pify: 5.0.0 - semver: 7.5.3 + semver: 7.5.0 slash: 3.0.0 validate-npm-package-license: 3.0.4 validate-npm-package-name: 4.0.0 @@ -5111,6 +5325,10 @@ packages: '@napi-rs/simple-git-win32-x64-msvc': 0.1.8 dev: false + /@next/env@13.4.11: + resolution: {integrity: sha512-BbPLOkfFFca+OpwSmGhNrl56B3ZzOFS4eJ85SpGTDpjlXoz7XFfS3MMOfjUCTuW1laTrQjm98I9Q30tsYpr8ew==} + dev: true + /@next/env@13.4.7: resolution: {integrity: sha512-ZlbiFulnwiFsW9UV1ku1OvX/oyIPLtMk9p/nnvDSwI0s7vSoZdRtxXNsaO+ZXrLv/pMbXVGq4lL8TbY9iuGmVw==} @@ -5192,6 +5410,11 @@ packages: requiresBuild: true optional: true + /@nicolo-ribaudo/semver-v6@6.3.3: + resolution: {integrity: sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==} + hasBin: true + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -5244,7 +5467,7 @@ packages: promise-all-reject-late: 1.0.1 promise-call-limit: 1.0.2 read-package-json-fast: 3.0.2 - semver: 7.5.3 + semver: 7.5.0 ssri: 10.0.4 treeverse: 3.0.0 walk-up-path: 1.0.0 @@ -5257,21 +5480,21 @@ packages: resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} dependencies: '@gar/promisify': 1.1.3 - semver: 7.5.3 + semver: 7.5.0 /@npmcli/fs@2.1.2: resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} dependencies: '@gar/promisify': 1.1.3 - semver: 7.5.3 + semver: 7.5.0 dev: true /@npmcli/fs@3.1.0: resolution: {integrity: sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dependencies: - semver: 7.5.3 + semver: 7.5.0 dev: true /@npmcli/git@4.0.4: @@ -5284,7 +5507,7 @@ packages: proc-log: 3.0.0 promise-inflight: 1.0.1(bluebird@3.7.2) promise-retry: 2.0.1 - semver: 7.5.3 + semver: 7.5.0 which: 3.0.1 transitivePeerDependencies: - bluebird @@ -5316,7 +5539,7 @@ packages: cacache: 17.1.0 json-parse-even-better-errors: 3.0.0 pacote: 15.1.1 - semver: 7.5.3 + semver: 7.5.0 transitivePeerDependencies: - bluebird - supports-color @@ -5697,7 +5920,7 @@ packages: engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} dependencies: cross-spawn: 7.0.3 - fast-glob: 3.2.12 + fast-glob: 3.3.0 is-glob: 4.0.3 open: 9.1.0 picocolors: 1.0.0 @@ -5763,7 +5986,7 @@ packages: node-stream-zip: 1.15.0 ora: 5.4.1 prompts: 2.4.2 - semver: 6.3.0 + semver: 6.3.1 strip-ansi: 5.2.0 sudo-prompt: 9.2.1 wcwidth: 1.0.1 @@ -5885,7 +6108,7 @@ packages: node-fetch: 2.6.9 open: 6.4.0 ora: 5.4.1 - semver: 6.3.0 + semver: 6.3.1 shell-quote: 1.8.1 transitivePeerDependencies: - encoding @@ -5918,7 +6141,7 @@ packages: fs-extra: 8.1.0 graceful-fs: 4.2.11 prompts: 2.4.2 - semver: 6.3.0 + semver: 6.3.1 transitivePeerDependencies: - '@babel/core' - bufferutil @@ -6361,7 +6584,7 @@ packages: lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 3.2.4(postcss@8.4.24) + tailwindcss: 3.2.4(postcss@8.4.26) dev: false /@testing-library/dom@8.20.0: @@ -6793,7 +7016,7 @@ packages: dev: true optional: true - /@typescript-eslint/eslint-plugin@5.59.2(@typescript-eslint/parser@5.59.2)(eslint@8.39.0)(typescript@4.9.5): + /@typescript-eslint/eslint-plugin@5.59.2(@typescript-eslint/parser@5.62.0)(eslint@8.39.0)(typescript@4.9.5): resolution: {integrity: sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -6805,7 +7028,7 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.5.1 - '@typescript-eslint/parser': 5.59.2(eslint@8.39.0)(typescript@5.0.4) + '@typescript-eslint/parser': 5.62.0(eslint@8.39.0)(typescript@4.9.5) '@typescript-eslint/scope-manager': 5.59.2 '@typescript-eslint/type-utils': 5.59.2(eslint@8.39.0)(typescript@4.9.5) '@typescript-eslint/utils': 5.59.2(eslint@8.39.0)(typescript@4.9.5) @@ -6821,8 +7044,28 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@5.59.2(eslint@8.39.0)(typescript@4.9.5): - resolution: {integrity: sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==} + /@typescript-eslint/parser@5.59.2(eslint@8.39.0)(typescript@5.0.4): + 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@5.0.4) + debug: 4.3.4(supports-color@6.1.0) + eslint: 8.39.0 + typescript: 5.0.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@5.62.0(eslint@8.39.0)(typescript@4.9.5): + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -6831,9 +7074,9 @@ packages: 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) + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) debug: 4.3.4(supports-color@6.1.0) eslint: 8.39.0 typescript: 4.9.5 @@ -6841,8 +7084,8 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@5.59.2(eslint@8.39.0)(typescript@5.0.4): - resolution: {integrity: sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==} + /@typescript-eslint/parser@5.62.0(eslint@8.39.0)(typescript@5.0.4): + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -6851,9 +7094,9 @@ packages: 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@5.0.4) + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.0.4) debug: 4.3.4(supports-color@6.1.0) eslint: 8.39.0 typescript: 5.0.4 @@ -6869,6 +7112,14 @@ 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/type-utils@5.59.2(eslint@8.39.0)(typescript@4.9.5): resolution: {integrity: sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -6894,6 +7145,11 @@ 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/typescript-estree@5.59.2(typescript@4.9.5): resolution: {integrity: sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -6936,6 +7192,48 @@ packages: - 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.0 + tsutils: 3.21.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.0.4): + 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.0 + tsutils: 3.21.0(typescript@5.0.4) + typescript: 5.0.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/utils@5.59.2(eslint@8.39.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} @@ -6950,7 +7248,7 @@ packages: '@typescript-eslint/typescript-estree': 5.59.2(typescript@4.9.5) eslint: 8.39.0 eslint-scope: 5.1.1 - semver: 7.5.3 + semver: 7.5.4 transitivePeerDependencies: - supports-color - typescript @@ -6981,7 +7279,15 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: '@typescript-eslint/types': 5.59.2 - eslint-visitor-keys: 3.4.0 + eslint-visitor-keys: 3.4.1 + 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.1 dev: true /@urql/core@2.3.6(graphql@15.8.0): @@ -7950,7 +8256,7 @@ packages: postcss-value-parser: 4.2.0 dev: true - /autoprefixer@10.4.0(postcss@8.4.24): + /autoprefixer@10.4.0(postcss@8.4.26): resolution: {integrity: sha512-7FdJ1ONtwzV1G43GDD0kpVMn/qbiNqyOPMFTX5nRffI+7vgWoFEc6DcXOxHJxrWNDXrZh18eDsZjvZGUljSRGA==} engines: {node: ^10 || ^12 || >=14} hasBin: true @@ -7962,7 +8268,7 @@ packages: fraction.js: 4.2.0 normalize-range: 0.1.2 picocolors: 1.0.0 - postcss: 8.4.24 + postcss: 8.4.26 postcss-value-parser: 4.2.0 dev: true @@ -8129,7 +8435,7 @@ packages: '@babel/compat-data': 7.22.5 '@babel/core': 7.21.8 '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.21.8) - semver: 6.3.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -8141,36 +8447,36 @@ packages: '@babel/compat-data': 7.22.5 '@babel/core': 7.22.5 '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.22.5) - semver: 6.3.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color dev: false - /babel-plugin-polyfill-corejs2@0.4.3(@babel/core@7.21.8): + /babel-plugin-polyfill-corejs2@0.4.3(@babel/core@7.22.5): resolution: {integrity: sha512-bM3gHc337Dta490gg+/AseNB9L4YLHxq1nGKZZSHbhXv4aTYU2MD2cjza1Ru4S6975YLTaL1K8uJf6ukJhhmtw==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: '@babel/compat-data': 7.22.5 - '@babel/core': 7.21.8 - '@babel/helper-define-polyfill-provider': 0.4.0(@babel/core@7.21.8) + '@babel/core': 7.22.5 + '@babel/helper-define-polyfill-provider': 0.4.0(@babel/core@7.22.5) semver: 6.3.0 transitivePeerDependencies: - supports-color - dev: false + dev: true - /babel-plugin-polyfill-corejs2@0.4.3(@babel/core@7.22.5): - resolution: {integrity: sha512-bM3gHc337Dta490gg+/AseNB9L4YLHxq1nGKZZSHbhXv4aTYU2MD2cjza1Ru4S6975YLTaL1K8uJf6ukJhhmtw==} + /babel-plugin-polyfill-corejs2@0.4.4(@babel/core@7.21.8): + resolution: {integrity: sha512-9WeK9snM1BfxB38goUEv2FLnA6ja07UMfazFHzCXUb3NyDZAwfXvQiURQ6guTTMeHcOsdknULm1PDhs4uWtKyA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.22.5 - '@babel/core': 7.22.5 - '@babel/helper-define-polyfill-provider': 0.4.0(@babel/core@7.22.5) - semver: 6.3.0 + '@babel/compat-data': 7.22.9 + '@babel/core': 7.21.8 + '@babel/helper-define-polyfill-provider': 0.4.1(@babel/core@7.21.8) + '@nicolo-ribaudo/semver-v6': 6.3.3 transitivePeerDependencies: - supports-color - dev: true + dev: false /babel-plugin-polyfill-corejs3@0.6.0(@babel/core@7.21.8): resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==} @@ -8195,29 +8501,29 @@ packages: - supports-color dev: false - /babel-plugin-polyfill-corejs3@0.8.1(@babel/core@7.21.8): + /babel-plugin-polyfill-corejs3@0.8.1(@babel/core@7.22.5): resolution: {integrity: sha512-ikFrZITKg1xH6pLND8zT14UPgjKHiGLqex7rGEZCH2EvhsneJaJPemmpQaIZV5AL03II+lXylw3UmddDK8RU5Q==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.8 - '@babel/helper-define-polyfill-provider': 0.4.0(@babel/core@7.21.8) + '@babel/core': 7.22.5 + '@babel/helper-define-polyfill-provider': 0.4.0(@babel/core@7.22.5) core-js-compat: 3.31.0 transitivePeerDependencies: - supports-color - dev: false + dev: true - /babel-plugin-polyfill-corejs3@0.8.1(@babel/core@7.22.5): - resolution: {integrity: sha512-ikFrZITKg1xH6pLND8zT14UPgjKHiGLqex7rGEZCH2EvhsneJaJPemmpQaIZV5AL03II+lXylw3UmddDK8RU5Q==} + /babel-plugin-polyfill-corejs3@0.8.2(@babel/core@7.21.8): + resolution: {integrity: sha512-Cid+Jv1BrY9ReW9lIfNlNpsI53N+FN7gE+f73zLAUbr9C52W4gKLWSByx47pfDJsEysojKArqOtOKZSVIIUTuQ==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.5 - '@babel/helper-define-polyfill-provider': 0.4.0(@babel/core@7.22.5) - core-js-compat: 3.31.0 + '@babel/core': 7.21.8 + '@babel/helper-define-polyfill-provider': 0.4.1(@babel/core@7.21.8) + core-js-compat: 3.31.1 transitivePeerDependencies: - supports-color - dev: true + dev: false /babel-plugin-polyfill-regenerator@0.3.1(@babel/core@7.22.5): resolution: {integrity: sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==} @@ -8251,27 +8557,27 @@ packages: - supports-color dev: false - /babel-plugin-polyfill-regenerator@0.5.0(@babel/core@7.21.8): + /babel-plugin-polyfill-regenerator@0.5.0(@babel/core@7.22.5): resolution: {integrity: sha512-hDJtKjMLVa7Z+LwnTCxoDLQj6wdc+B8dun7ayF2fYieI6OzfuvcLMB32ihJZ4UhCBwNYGl5bg/x/P9cMdnkc2g==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.8 - '@babel/helper-define-polyfill-provider': 0.4.0(@babel/core@7.21.8) + '@babel/core': 7.22.5 + '@babel/helper-define-polyfill-provider': 0.4.0(@babel/core@7.22.5) transitivePeerDependencies: - supports-color - dev: false + dev: true - /babel-plugin-polyfill-regenerator@0.5.0(@babel/core@7.22.5): - resolution: {integrity: sha512-hDJtKjMLVa7Z+LwnTCxoDLQj6wdc+B8dun7ayF2fYieI6OzfuvcLMB32ihJZ4UhCBwNYGl5bg/x/P9cMdnkc2g==} + /babel-plugin-polyfill-regenerator@0.5.1(@babel/core@7.21.8): + resolution: {integrity: sha512-L8OyySuI6OSQ5hFy9O+7zFjyr4WhAfRjLIOkhQGYl+emwJkd/S4XXT1JpfrgR1jrQ1NcGiOh+yAdGlF8pnC3Jw==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.5 - '@babel/helper-define-polyfill-provider': 0.4.0(@babel/core@7.22.5) + '@babel/core': 7.21.8 + '@babel/helper-define-polyfill-provider': 0.4.1(@babel/core@7.21.8) transitivePeerDependencies: - supports-color - dev: true + dev: false /babel-plugin-react-native-web@0.18.12: resolution: {integrity: sha512-4djr9G6fMdwQoD6LQ7hOKAm39+y12flWgovAqS1k5O8f42YQ3A1FFMyV5kKfetZuGhZO5BmNmOdRRZQ1TixtDw==} @@ -8827,7 +9133,7 @@ packages: /builtins@5.0.1: resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} dependencies: - semver: 7.5.3 + semver: 7.5.0 dev: true /bundle-name@3.0.0: @@ -9559,7 +9865,7 @@ packages: js-string-escape: 1.0.1 lodash: 4.17.21 md5-hex: 3.0.1 - semver: 7.5.0 + semver: 7.5.4 well-known-symbols: 2.0.0 dev: true @@ -9657,7 +9963,7 @@ packages: json-stringify-safe: 5.0.1 lodash: 4.17.21 meow: 8.1.2 - semver: 6.3.0 + semver: 6.3.1 split: 1.0.1 through2: 4.0.2 dev: true @@ -9743,7 +10049,7 @@ packages: webpack: ^4.37.0 || ^5.0.0 dependencies: cacache: 15.3.0 - fast-glob: 3.2.12 + fast-glob: 3.3.0 find-cache-dir: 3.3.2 glob-parent: 5.1.2 globby: 11.1.0 @@ -9763,6 +10069,12 @@ packages: dependencies: browserslist: 4.21.9 + /core-js-compat@3.31.1: + resolution: {integrity: sha512-wIDWd2s5/5aJSdpOJHfSibxNODxoGoWOBHt8JSPB41NOE94M7kuTPZCYLOlTtuoXTsBPKobpJ6T+y0SSy5L9SA==} + dependencies: + browserslist: 4.21.9 + dev: false + /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -9859,7 +10171,7 @@ packages: dependencies: nice-try: 1.0.5 path-key: 2.0.1 - semver: 5.7.1 + semver: 5.7.2 shebang-command: 1.2.0 which: 1.3.1 dev: false @@ -9937,7 +10249,7 @@ packages: postcss-modules-values: 3.0.0 postcss-value-parser: 4.2.0 schema-utils: 2.7.1 - semver: 6.3.0 + semver: 6.3.1 webpack: 4.43.0 dev: false @@ -10985,7 +11297,7 @@ packages: engines: {node: '>=12'} dev: true - /dts-cli@1.4.0(@babel/plugin-syntax-flow@7.21.4)(@babel/plugin-transform-react-jsx@7.21.5)(@types/node@17.0.23): + /dts-cli@1.4.0(@babel/plugin-syntax-flow@7.22.5)(@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 @@ -11002,8 +11314,8 @@ packages: '@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.59.2(@typescript-eslint/parser@5.59.2)(eslint@8.39.0)(typescript@4.9.5) - '@typescript-eslint/parser': 5.59.2(eslint@8.39.0)(typescript@4.9.5) + '@typescript-eslint/eslint-plugin': 5.59.2(@typescript-eslint/parser@5.62.0)(eslint@8.39.0)(typescript@4.9.5) + '@typescript-eslint/parser': 5.62.0(eslint@8.39.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) @@ -11017,8 +11329,8 @@ packages: enquirer: 2.3.6 eslint: 8.39.0 eslint-config-prettier: 8.8.0(eslint@8.39.0) - eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.21.4)(@babel/plugin-transform-react-jsx@7.21.5)(eslint@8.39.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.2)(eslint-import-resolver-typescript@3.5.5)(eslint@8.39.0) + eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.5)(eslint@8.39.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0)(eslint@8.39.0) eslint-plugin-jest: 26.9.0(@typescript-eslint/eslint-plugin@5.59.2)(eslint@8.39.0)(jest@27.5.1)(typescript@4.9.5) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.39.0) eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.8.0)(eslint@8.39.0)(prettier@2.8.8) @@ -11452,12 +11764,12 @@ packages: eslint: ^8.0.0 dependencies: '@rushstack/eslint-patch': 1.2.0 - '@typescript-eslint/eslint-plugin': 5.59.2(@typescript-eslint/parser@5.59.2)(eslint@8.39.0)(typescript@4.9.5) - '@typescript-eslint/parser': 5.59.2(eslint@8.39.0)(typescript@5.0.4) + '@typescript-eslint/eslint-plugin': 5.59.2(@typescript-eslint/parser@5.62.0)(eslint@8.39.0)(typescript@4.9.5) + '@typescript-eslint/parser': 5.62.0(eslint@8.39.0)(typescript@5.0.4) confusing-browser-globals: 1.0.11 eslint: 8.39.0 eslint-plugin-css-modules: 2.11.0(eslint@8.39.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.2)(eslint-import-resolver-typescript@3.5.5)(eslint@8.39.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0)(eslint@8.39.0) eslint-plugin-jest: 26.9.0(@typescript-eslint/eslint-plugin@5.59.2)(eslint@8.39.0)(jest@27.5.1)(typescript@4.9.5) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.39.0) eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.8.0)(eslint@8.39.0)(prettier@2.8.8) @@ -11482,12 +11794,12 @@ packages: eslint: ^8.0.0 dependencies: '@rushstack/eslint-patch': 1.2.0 - '@typescript-eslint/eslint-plugin': 5.59.2(@typescript-eslint/parser@5.59.2)(eslint@8.39.0)(typescript@4.9.5) - '@typescript-eslint/parser': 5.59.2(eslint@8.39.0)(typescript@5.0.4) + '@typescript-eslint/eslint-plugin': 5.59.2(@typescript-eslint/parser@5.62.0)(eslint@8.39.0)(typescript@4.9.5) + '@typescript-eslint/parser': 5.62.0(eslint@8.39.0)(typescript@4.9.5) confusing-browser-globals: 1.0.11 eslint: 8.39.0 eslint-plugin-css-modules: 2.11.0(eslint@8.39.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.2)(eslint-import-resolver-typescript@3.5.5)(eslint@8.39.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0)(eslint@8.39.0) eslint-plugin-jest: 26.9.0(@typescript-eslint/eslint-plugin@5.59.2)(eslint@8.39.0)(jest@29.5.0)(typescript@4.9.5) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.39.0) eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.8.0)(eslint@8.39.0)(prettier@2.8.8) @@ -11562,7 +11874,7 @@ packages: 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.39.0) eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.2)(eslint-import-resolver-typescript@3.5.5)(eslint@8.39.0) get-tsconfig: 4.5.0 - globby: 13.1.4 + globby: 13.2.2 is-core-module: 2.12.0 is-glob: 4.0.3 synckit: 0.8.5 @@ -11603,6 +11915,35 @@ packages: - supports-color dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint@8.39.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': 5.62.0(eslint@8.39.0)(typescript@4.9.5) + debug: 3.2.7(supports-color@6.1.0) + eslint: 8.39.0 + eslint-import-resolver-node: 0.3.7 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-plugin-css-modules@2.11.0(eslint@8.39.0): resolution: {integrity: sha512-CLvQvJOMlCywZzaI4HVu7QH/ltgNXvCg7giJGiE+sA9wh5zQ+AqTgftAzrERV22wHe1p688wrU/Zwxt1Ry922w==} engines: {node: '>=4.0.0'} @@ -11629,7 +11970,7 @@ packages: - supports-color dev: true - /eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.21.4)(@babel/plugin-transform-react-jsx@7.21.5)(eslint@8.39.0): + /eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.5)(eslint@8.39.0): resolution: {integrity: sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==} engines: {node: '>=12.0.0'} peerDependencies: @@ -11637,8 +11978,8 @@ packages: '@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.21.5(@babel/core@7.22.5) + '@babel/plugin-syntax-flow': 7.22.5(@babel/core@7.22.9) + '@babel/plugin-transform-react-jsx': 7.22.5(@babel/core@7.22.9) eslint: 8.39.0 lodash: 4.17.21 string-natural-compare: 3.0.1 @@ -11677,6 +12018,39 @@ packages: - supports-color dev: true + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0)(eslint@8.39.0): + resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.39.0)(typescript@4.9.5) + array-includes: 3.1.6 + array.prototype.flat: 1.3.1 + array.prototype.flatmap: 1.3.1 + debug: 3.2.7(supports-color@6.1.0) + doctrine: 2.1.0 + eslint: 8.39.0 + eslint-import-resolver-node: 0.3.7 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint@8.39.0) + has: 1.0.3 + is-core-module: 2.12.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.values: 1.1.6 + resolve: 1.22.2 + semver: 6.3.0 + tsconfig-paths: 3.14.2 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + /eslint-plugin-jest@26.9.0(@typescript-eslint/eslint-plugin@5.59.2)(eslint@8.39.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} @@ -11690,7 +12064,7 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.59.2(@typescript-eslint/parser@5.59.2)(eslint@8.39.0)(typescript@4.9.5) + '@typescript-eslint/eslint-plugin': 5.59.2(@typescript-eslint/parser@5.62.0)(eslint@8.39.0)(typescript@4.9.5) '@typescript-eslint/utils': 5.59.2(eslint@8.39.0)(typescript@4.9.5) eslint: 8.39.0 jest: 27.5.1(ts-node@10.9.1) @@ -11712,7 +12086,7 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.59.2(@typescript-eslint/parser@5.59.2)(eslint@8.39.0)(typescript@4.9.5) + '@typescript-eslint/eslint-plugin': 5.59.2(@typescript-eslint/parser@5.62.0)(eslint@8.39.0)(typescript@4.9.5) '@typescript-eslint/utils': 5.59.2(eslint@8.39.0)(typescript@4.9.5) eslint: 8.39.0 jest: 29.5.0(@types/node@17.0.23) @@ -11886,6 +12260,11 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /eslint-visitor-keys@3.4.1: + resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /eslint@8.39.0: resolution: {integrity: sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -12294,7 +12673,7 @@ packages: dependencies: chalk: 4.1.2 commander: 7.2.0 - fast-glob: 3.2.12 + fast-glob: 3.3.0 find-up: 5.0.0 fs-extra: 9.1.0 dev: false @@ -12494,6 +12873,16 @@ packages: micromatch: 4.0.5 dev: true + /fast-glob@3.3.0: + resolution: {integrity: sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -12815,7 +13204,7 @@ packages: signal-exit: 4.0.1 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.1.6)(webpack@4.43.0): resolution: {integrity: sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==} engines: {node: '>=6.11.5', yarn: '>=1.0.0'} peerDependencies: @@ -12833,9 +13222,9 @@ packages: chalk: 2.4.2 micromatch: 3.1.10(supports-color@6.1.0) minimatch: 3.1.2 - semver: 5.7.1 + semver: 5.7.2 tapable: 1.1.3 - typescript: 5.0.4 + typescript: 5.1.6 webpack: 4.43.0 worker-rpc: 0.1.1 transitivePeerDependencies: @@ -13201,7 +13590,7 @@ packages: hasBin: true dependencies: meow: 8.1.2 - semver: 6.3.0 + semver: 6.3.1 dev: true /git-up@7.0.0: @@ -13378,7 +13767,7 @@ packages: '@types/glob': 7.2.0 array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.2.12 + fast-glob: 3.2.11 glob: 7.2.3 ignore: 5.2.4 merge2: 1.4.1 @@ -13392,7 +13781,7 @@ packages: '@types/glob': 7.2.0 array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.2.12 + fast-glob: 3.3.0 glob: 7.2.3 ignore: 5.2.4 merge2: 1.4.1 @@ -13405,7 +13794,7 @@ packages: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.2.12 + fast-glob: 3.3.0 ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 @@ -13422,12 +13811,12 @@ packages: merge2: 1.4.1 slash: 3.0.0 - /globby@13.1.4: - resolution: {integrity: sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==} + /globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: dir-glob: 3.0.1 - fast-glob: 3.2.12 + fast-glob: 3.3.0 ignore: 5.2.4 merge2: 1.4.1 slash: 4.0.0 @@ -14202,7 +14591,7 @@ packages: promzard: 0.3.0 read: 1.0.7 read-package-json: 5.0.1 - semver: 7.5.3 + semver: 7.5.0 validate-npm-package-license: 3.0.4 validate-npm-package-name: 4.0.0 dev: true @@ -14866,7 +15255,7 @@ packages: '@babel/parser': 7.22.5 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 - semver: 6.3.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color dev: true @@ -15685,7 +16074,7 @@ packages: jest-util: 29.5.0 natural-compare: 1.4.0 pretty-format: 29.5.0 - semver: 7.5.3 + semver: 7.5.4 transitivePeerDependencies: - supports-color dev: true @@ -15914,7 +16303,7 @@ packages: resolution: {integrity: sha512-KmxeBlRjwoqCnBBKGsihFtvsBHyUFlBxJPK4FzeYcIuBfdjv6jFys44JITAgSTbQD+vIdwMEfyZklsuQX0yI1Q==} dev: false - /jscodeshift@0.13.1(@babel/preset-env@7.22.5): + /jscodeshift@0.13.1(@babel/preset-env@7.22.9): resolution: {integrity: sha512-lGyiEbGOvmMRKgWk4vf+lUrCWO/8YR8sUR3FKF1Cq5fovjZDlIcw3Hu5ppLHAnEXshVffvaM0eyuY/AbOeYpnQ==} hasBin: true peerDependencies: @@ -15926,7 +16315,7 @@ packages: '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.22.5) '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.22.5) '@babel/plugin-transform-modules-commonjs': 7.22.5(@babel/core@7.22.5) - '@babel/preset-env': 7.22.5(@babel/core@7.21.8) + '@babel/preset-env': 7.22.9(@babel/core@7.21.8) '@babel/preset-flow': 7.21.4(@babel/core@7.22.5) '@babel/preset-typescript': 7.21.5(@babel/core@7.22.5) '@babel/register': 7.21.0(@babel/core@7.22.5) @@ -16364,7 +16753,7 @@ packages: npm-package-arg: 10.1.0 npm-registry-fetch: 14.0.5 proc-log: 3.0.0 - semver: 7.5.3 + semver: 7.5.0 sigstore: 1.4.0 ssri: 10.0.4 transitivePeerDependencies: @@ -16641,13 +17030,13 @@ packages: engines: {node: '>=6'} dependencies: pify: 4.0.1 - semver: 5.7.1 + semver: 5.7.2 /make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} dependencies: - semver: 6.3.0 + semver: 6.3.1 /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -18261,7 +18650,7 @@ packages: '@panva/hkdf': 1.1.1 cookie: 0.5.0 jose: 4.14.4 - next: 13.4.7(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + next: 13.4.7(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) oauth: 0.9.15 openid-client: 5.4.2 preact: 10.13.2 @@ -18295,12 +18684,12 @@ packages: react: '>=16.0.0' react-dom: '>=16.0.0' dependencies: - next: 13.4.7(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + next: 13.4.7(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /next-sitemap@4.0.7(@next/env@13.4.7)(next@13.4.7): + /next-sitemap@4.0.7(@next/env@13.4.11)(next@13.4.7): resolution: {integrity: sha512-S2g5IwJeO0+ecmFq981fb+Mw9YWmntOuN/qTCxclSkUibOJ8qKIOye0vn6NEJ1S4tKhbY+MTYKgJpNdFZYxLoA==} engines: {node: '>=14.18'} hasBin: true @@ -18309,9 +18698,9 @@ packages: next: '*' dependencies: '@corex/deepmerge': 4.0.43 - '@next/env': 13.4.7 + '@next/env': 13.4.11 minimist: 1.2.8 - next: 13.4.7(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + next: 13.4.7(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) dev: true /next-themes@0.2.1(next@13.4.7)(react-dom@18.2.0)(react@18.2.0): @@ -18321,12 +18710,12 @@ packages: react: '*' react-dom: '*' dependencies: - next: 13.4.7(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + next: 13.4.7(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /next@13.4.7(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0): + /next@13.4.7(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-M8z3k9VmG51SRT6v5uDKdJXcAqLzP3C+vaKfLIAM0Mhx1um1G7MDnO63+m52qPdZfrTFzMZNzfsgvm3ghuVHIQ==} engines: {node: '>=16.8.0'} hasBin: true @@ -18351,7 +18740,7 @@ packages: postcss: 8.4.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.1.1(@babel/core@7.22.5)(react@18.2.0) + styled-jsx: 5.1.1(@babel/core@7.22.9)(react@18.2.0) watchpack: 2.4.0 zod: 3.21.4 optionalDependencies: @@ -18384,7 +18773,7 @@ packages: git-url-parse: 13.1.0 intersection-observer: 0.12.2 match-sorter: 6.3.1 - next: 13.4.7(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + next: 13.4.7(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) next-seo: 6.0.0(next@13.4.7)(react-dom@18.2.0)(react@18.2.0) next-themes: 0.2.1(next@13.4.7)(react-dom@18.2.0)(react@18.2.0) nextra: 2.8.0(next@13.4.7)(react-dom@18.2.0)(react@18.2.0) @@ -18412,7 +18801,7 @@ packages: gray-matter: 4.0.3 katex: 0.16.7 lodash.get: 4.4.2 - next: 13.4.7(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + next: 13.4.7(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) next-mdx-remote: 4.4.1(react-dom@18.2.0)(react@18.2.0) p-limit: 3.1.0 react: 18.2.0 @@ -18512,7 +18901,7 @@ packages: nopt: 6.0.0 npmlog: 6.0.2 rimraf: 3.0.2 - semver: 7.5.3 + semver: 7.5.0 tar: 6.1.14 which: 2.0.2 transitivePeerDependencies: @@ -18601,7 +18990,7 @@ packages: dependencies: hosted-git-info: 2.8.9 resolve: 1.22.2 - semver: 5.7.1 + semver: 5.7.2 validate-npm-package-license: 3.0.4 dev: true @@ -18611,7 +19000,7 @@ packages: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.12.0 - semver: 7.5.3 + semver: 7.5.0 validate-npm-package-license: 3.0.4 dev: true @@ -18621,7 +19010,7 @@ packages: dependencies: hosted-git-info: 5.2.1 is-core-module: 2.12.0 - semver: 7.5.3 + semver: 7.5.0 validate-npm-package-license: 3.0.4 dev: true @@ -18631,7 +19020,7 @@ packages: dependencies: hosted-git-info: 6.1.1 is-core-module: 2.12.0 - semver: 7.5.3 + semver: 7.5.0 validate-npm-package-license: 3.0.4 dev: true @@ -18678,7 +19067,7 @@ packages: resolution: {integrity: sha512-dH3GmQL4vsPtld59cOn8uY0iOqRmqKvV+DLGwNXV/Q7MDgD2QfOADWd/mFXcIE5LVhYYGjA3baz6W9JneqnuCw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dependencies: - semver: 7.5.3 + semver: 7.5.0 dev: true /npm-normalize-package-bin@1.0.1: @@ -18696,7 +19085,7 @@ packages: dependencies: hosted-git-info: 6.1.1 proc-log: 3.0.0 - semver: 7.5.3 + semver: 7.5.0 validate-npm-package-name: 5.0.0 dev: true @@ -18705,7 +19094,7 @@ packages: dependencies: hosted-git-info: 3.0.8 osenv: 0.1.5 - semver: 5.7.1 + semver: 5.7.2 validate-npm-package-name: 3.0.0 dev: false @@ -18714,7 +19103,7 @@ packages: engines: {node: '>=10'} dependencies: hosted-git-info: 3.0.8 - semver: 7.5.3 + semver: 7.5.0 validate-npm-package-name: 3.0.0 dev: true @@ -18724,7 +19113,7 @@ packages: dependencies: hosted-git-info: 5.2.1 proc-log: 2.0.1 - semver: 7.5.3 + semver: 7.5.0 validate-npm-package-name: 4.0.0 dev: true @@ -18753,7 +19142,7 @@ packages: npm-install-checks: 6.1.1 npm-normalize-package-bin: 3.0.1 npm-package-arg: 10.1.0 - semver: 7.5.3 + semver: 7.5.0 dev: true /npm-registry-fetch@13.3.1: @@ -19611,6 +20000,10 @@ packages: /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + /path-to-regexp@6.2.1: + resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} + dev: false + /path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} engines: {node: '>=4'} @@ -19764,11 +20157,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.1.6): 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.1.6) transitivePeerDependencies: - typescript dev: false @@ -19865,13 +20258,13 @@ packages: resolve: 1.22.2 dev: false - /postcss-import@14.1.0(postcss@8.4.24): + /postcss-import@14.1.0(postcss@8.4.26): resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} engines: {node: '>=10.0.0'} peerDependencies: postcss: ^8.0.0 dependencies: - postcss: 8.4.24 + postcss: 8.4.26 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.2 @@ -19887,14 +20280,14 @@ packages: postcss: 8.4.23 dev: false - /postcss-js@4.0.1(postcss@8.4.24): + /postcss-js@4.0.1(postcss@8.4.26): resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} engines: {node: ^12 || ^14 || >= 16} peerDependencies: postcss: ^8.4.21 dependencies: camelcase-css: 2.0.1 - postcss: 8.4.24 + postcss: 8.4.26 dev: false /postcss-load-config@3.1.4(postcss@8.4.23): @@ -19914,7 +20307,7 @@ packages: yaml: 1.10.2 dev: false - /postcss-load-config@3.1.4(postcss@8.4.24): + /postcss-load-config@3.1.4(postcss@8.4.26): resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} engines: {node: '>= 10'} peerDependencies: @@ -19927,7 +20320,7 @@ packages: optional: true dependencies: lilconfig: 2.1.0 - postcss: 8.4.24 + postcss: 8.4.26 yaml: 1.10.2 dev: false @@ -20109,13 +20502,13 @@ packages: postcss-selector-parser: 6.0.12 dev: false - /postcss-nested@6.0.0(postcss@8.4.24): + /postcss-nested@6.0.0(postcss@8.4.26): resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==} engines: {node: '>=12.0'} peerDependencies: postcss: ^8.2.14 dependencies: - postcss: 8.4.24 + postcss: 8.4.26 postcss-selector-parser: 6.0.12 dev: false @@ -20316,6 +20709,15 @@ packages: nanoid: 3.3.6 picocolors: 1.0.0 source-map-js: 1.0.2 + dev: true + + /postcss@8.4.26: + resolution: {integrity: sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 /preact-render-to-string@5.2.6(preact@10.13.2): resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} @@ -20347,7 +20749,7 @@ packages: fast-diff: 1.2.0 dev: true - /prettier-plugin-tailwindcss@0.2.3(prettier@2.8.8): + /prettier-plugin-tailwindcss@0.2.3(prettier@3.0.0): resolution: {integrity: sha512-s2N5Dh7Ao5KTV1mao5ZBnn8EKtUcDPJEkGViZIjI0Ij9TTI5zgTz4IHOxW33jOdjHKa8CSjM88scelUiC5TNRQ==} engines: {node: '>=12.17.0'} peerDependencies: @@ -20399,7 +20801,7 @@ packages: prettier-plugin-twig-melody: optional: true dependencies: - prettier: 2.8.8 + prettier: 3.0.0 dev: true /prettier@2.7.1: @@ -20414,6 +20816,12 @@ packages: hasBin: true dev: true + /prettier@3.0.0: + resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==} + engines: {node: '>=14'} + hasBin: true + dev: true + /pretty-bytes@5.6.0: resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} engines: {node: '>=6'} @@ -20780,7 +21188,7 @@ packages: 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.1.6)(webpack@4.43.0): resolution: {integrity: sha512-dx0LvIGHcOPtKbeiSUM4jqpBl3TcY7CDjZdfOIcKeznE7BWr9dg0iPG90G5yfVQ+p/rGNMXdbfStvzQZEVEi4A==} engines: {node: '>=10'} peerDependencies: @@ -20799,7 +21207,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.1.6)(webpack@4.43.0) global-modules: 2.0.0 globby: 11.0.1 gzip-size: 5.1.1 @@ -20814,7 +21222,7 @@ packages: shell-quote: 1.7.2 strip-ansi: 6.0.0 text-table: 0.2.0 - typescript: 5.0.4 + typescript: 5.1.6 webpack: 4.43.0 transitivePeerDependencies: - eslint @@ -20865,12 +21273,12 @@ packages: /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} - /react-native-codegen@0.70.6(@babel/preset-env@7.22.5): + /react-native-codegen@0.70.6(@babel/preset-env@7.22.9): resolution: {integrity: sha512-kdwIhH2hi+cFnG5Nb8Ji2JwmcCxnaOOo9440ov7XDzSvGfmUStnCzl+MCW8jLjqHcE4icT7N9y+xx4f50vfBTw==} dependencies: '@babel/parser': 7.21.8 flow-parser: 0.121.0 - jscodeshift: 0.13.1(@babel/preset-env@7.22.5) + jscodeshift: 0.13.1(@babel/preset-env@7.22.9) nullthrows: 1.1.1 transitivePeerDependencies: - '@babel/preset-env' @@ -20900,7 +21308,7 @@ packages: - encoding dev: false - /react-native@0.70.5(@babel/core@7.21.8)(@babel/preset-env@7.22.5)(react@18.1.0): + /react-native@0.70.5(@babel/core@7.21.8)(@babel/preset-env@7.22.9)(react@18.1.0): resolution: {integrity: sha512-5NZM80LC3L+TIgQX/09yiyy48S73wMgpIgN5cCv3XTMR394+KpDI3rBZGH4aIgWWuwijz31YYVF5504+9n2Zfw==} engines: {node: '>=14'} hasBin: true @@ -20930,7 +21338,7 @@ packages: promise: 8.3.0 react: 18.1.0 react-devtools-core: 4.24.0 - react-native-codegen: 0.70.6(@babel/preset-env@7.22.5) + react-native-codegen: 0.70.6(@babel/preset-env@7.22.9) react-native-gradle-plugin: 0.70.3 react-refresh: 0.4.3 react-shallow-renderer: 16.15.0(react@18.1.0) @@ -21897,14 +22305,18 @@ packages: node-forge: 0.10.0 dev: false - /semver@5.7.1: - resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} + /semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true /semver@6.3.0: resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} hasBin: true + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + /semver@7.3.2: resolution: {integrity: sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==} engines: {node: '>=10'} @@ -21939,6 +22351,14 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 + dev: true + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 /send@0.18.0(supports-color@6.1.0): resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} @@ -22785,7 +23205,7 @@ packages: dependencies: inline-style-parser: 0.1.1 - /styled-jsx@5.1.1(@babel/core@7.22.5)(react@18.2.0): + /styled-jsx@5.1.1(@babel/core@7.22.9)(react@18.2.0): resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} peerDependencies: @@ -22798,7 +23218,7 @@ packages: babel-plugin-macros: optional: true dependencies: - '@babel/core': 7.22.5 + '@babel/core': 7.22.9 client-only: 0.0.1 react: 18.2.0 @@ -22954,7 +23374,7 @@ packages: - ts-node dev: false - /tailwindcss@3.2.4(postcss@8.4.24): + /tailwindcss@3.2.4(postcss@8.4.26): resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==} engines: {node: '>=12.13.0'} hasBin: true @@ -22975,11 +23395,11 @@ packages: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.0 - postcss: 8.4.24 - postcss-import: 14.1.0(postcss@8.4.24) - postcss-js: 4.0.1(postcss@8.4.24) - postcss-load-config: 3.1.4(postcss@8.4.24) - postcss-nested: 6.0.0(postcss@8.4.24) + postcss: 8.4.26 + postcss-import: 14.1.0(postcss@8.4.26) + postcss-js: 4.0.1(postcss@8.4.26) + postcss-load-config: 3.1.4(postcss@8.4.26) + postcss-nested: 6.0.0(postcss@8.4.26) postcss-selector-parser: 6.0.12 postcss-value-parser: 4.2.0 quick-lru: 5.1.1 @@ -23486,7 +23906,7 @@ packages: yn: 3.1.1 dev: true - /ts-pnp@1.2.0(typescript@5.0.4): + /ts-pnp@1.2.0(typescript@5.1.6): resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==} engines: {node: '>=6'} peerDependencies: @@ -23495,7 +23915,7 @@ packages: typescript: optional: true dependencies: - typescript: 5.0.4 + typescript: 5.1.6 dev: false /tsconfig-paths@3.14.2: @@ -23732,6 +24152,13 @@ packages: resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} engines: {node: '>=12.20'} hasBin: true + dev: true + + /typescript@5.1.6: + resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} + engines: {node: '>=14.17'} + hasBin: true + dev: false /ua-parser-js@0.7.35: resolution: {integrity: sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==} @@ -24595,7 +25022,7 @@ packages: portfinder: 1.0.32(supports-color@6.1.0) schema-utils: 1.0.0 selfsigned: 1.10.14 - semver: 6.3.0 + semver: 6.3.1 serve-index: 1.9.1(supports-color@6.1.0) sockjs: 0.3.20 sockjs-client: 1.4.0(supports-color@6.1.0) From b9e05a4381e62a63e3d8554861125472f8812f07 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 21 Jul 2023 14:41:16 +0200 Subject: [PATCH 02/37] Update example --- examples/example-next-13/messages/de.json | 2 +- examples/example-next-13/messages/en.json | 2 +- examples/example-next-13/src/app/[locale]/about/page.tsx | 2 +- examples/example-next-13/src/middleware.tsx | 9 ++++++++- packages/next-intl/middleware.d.ts | 4 +++- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/examples/example-next-13/messages/de.json b/examples/example-next-13/messages/de.json index bc81a392f..ca70e44db 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 /about in der Adressleiste des Browsers auf dieser Seite.

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

" }, "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..f7260daf0 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 /about in the browser address bar on this page.

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

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

{chunks}

, code: (chunks) => ( diff --git a/examples/example-next-13/src/middleware.tsx b/examples/example-next-13/src/middleware.tsx index 0c1479934..90d739f71 100644 --- a/examples/example-next-13/src/middleware.tsx +++ b/examples/example-next-13/src/middleware.tsx @@ -2,7 +2,14 @@ import createMiddleware from 'next-intl/middleware'; export default createMiddleware({ locales: ['en', 'de'], - defaultLocale: 'en' + defaultLocale: 'en', + pathnames: { + home: '/', + about: { + en: '/about', + de: '/ueber' + } + } }); export const config = { diff --git a/packages/next-intl/middleware.d.ts b/packages/next-intl/middleware.d.ts index 0b95aa6d8..420a5a617 100644 --- a/packages/next-intl/middleware.d.ts +++ b/packages/next-intl/middleware.d.ts @@ -1,3 +1,5 @@ -import createMiddleware from './dist/middleware'; +// dts-cli still uses TypeScript 4 and isn't able to +// compile the types for the middlware correctly. +import createMiddleware from './dist/src/middleware'; export = createMiddleware; From f4dd8fad300ccda280baeb2466b45ed28ff4ba2e Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 21 Jul 2023 15:03:36 +0200 Subject: [PATCH 03/37] More redirects --- .../src/components/NavigationLink.tsx | 2 + .../src/middleware/LocalizedPathnames.tsx | 54 ++++++------------- .../test/middleware/middleware.test.tsx | 22 +++++--- 3 files changed, 33 insertions(+), 45 deletions(-) diff --git a/examples/example-next-13/src/components/NavigationLink.tsx b/examples/example-next-13/src/components/NavigationLink.tsx index 689bc6cd4..3716ff0ec 100644 --- a/examples/example-next-13/src/components/NavigationLink.tsx +++ b/examples/example-next-13/src/components/NavigationLink.tsx @@ -11,6 +11,8 @@ type Props = Omit, 'href'> & { export default function NavigationLink({href, ...rest}: Props) { const pathname = usePathname(); + + // TODO: We need to consult the pathnames map here const isActive = pathname === href; return ( diff --git a/packages/next-intl/src/middleware/LocalizedPathnames.tsx b/packages/next-intl/src/middleware/LocalizedPathnames.tsx index d058c9d3d..5701bb4f2 100644 --- a/packages/next-intl/src/middleware/LocalizedPathnames.tsx +++ b/packages/next-intl/src/middleware/LocalizedPathnames.tsx @@ -24,52 +24,32 @@ export function getLocalizedRedirectPathname( configWithDefaults.locales ); - if (pathLocale) { - if (pathLocale === configWithDefaults.defaultLocale) { - return; + for (const [, routePath] of Object.entries(configWithDefaults.pathnames)) { + if (typeof routePath === 'string') { + // No redirect is necessary if all locales use the same pathname + continue; } - for (const [, routePath] of Object.entries(configWithDefaults.pathnames)) { - if (typeof routePath === 'string') { - // No redirect is necessary if all locales use the same pathname + for (const [locale, localePathname] of Object.entries(routePath)) { + if (resolvedLocale === locale) { continue; } - const defaultLocaleTemplate = routePath[configWithDefaults.defaultLocale]; - const pathLocalePathname = `/${pathLocale}${defaultLocaleTemplate}`; - const matches = matchesPathname(pathLocalePathname, pathname); + let template = ''; + if (pathLocale) template = `/${pathLocale}`; + template += localePathname; + const matches = matchesPathname(template, pathname); if (matches) { - const params = getRouteParams(pathLocalePathname, pathname); - return getPathWithSearch( - `/${pathLocale}` + formatPathname(routePath[pathLocale], params), - request.nextUrl.search - ); - } - } - } else if (resolvedLocale !== configWithDefaults.defaultLocale) { - if (resolvedLocale === configWithDefaults.defaultLocale) { - return; - } + const params = getRouteParams(template, pathname); - // Check if the path matches a route from the default locale. - // If this is the case, then redirect to a localized version. - for (const [, routePath] of Object.entries(configWithDefaults.pathnames)) { - if (typeof routePath === 'string') { - // No redirect is necessary if all locales use the same pathname - continue; - } + let targetPathname = ''; + if (resolvedLocale !== configWithDefaults.defaultLocale || pathLocale) { + targetPathname = `/${resolvedLocale}`; + } + targetPathname += formatPathname(routePath[resolvedLocale], params); - const defaultLocalePathname = routePath[configWithDefaults.defaultLocale]; - const matches = matchesPathname(defaultLocalePathname, pathname); - - if (matches) { - const params = getRouteParams(defaultLocalePathname, pathname); - return getPathWithSearch( - `/${resolvedLocale}` + - formatPathname(routePath[resolvedLocale], params), - request.nextUrl.search - ); + return getPathWithSearch(targetPathname, request.nextUrl.search); } } } diff --git a/packages/next-intl/test/middleware/middleware.test.tsx b/packages/next-intl/test/middleware/middleware.test.tsx index ddef7cdea..66b74928f 100644 --- a/packages/next-intl/test/middleware/middleware.test.tsx +++ b/packages/next-intl/test/middleware/middleware.test.tsx @@ -310,13 +310,13 @@ describe('prefix-based routing', () => { ); }); - it('forwards a request for a localized route that is not associated with the requested locale so that a 404 response can be returned', () => { + it('redirects a request for a localized route that is not associated with the requested locale', () => { middlewareWithPathnames(createMockRequest('/über', 'en')); expect(MockedNextResponse.next).not.toHaveBeenCalled(); - expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.rewrite).toHaveBeenCalledTimes(1); - expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe( - 'http://localhost:3000/en/%C3%BCber' + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect).toHaveBeenCalledTimes(1); + expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/about' ); }); @@ -516,13 +516,19 @@ describe('prefix-based routing', () => { ); }); - it('forwards a request for a localized route that is not associated with the requested locale so that a 404 response can be returned', () => { + 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/über', 'en')); middlewareWithPathnames(createMockRequest('/en/benutzer/12', 'en')); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); - expect(MockedNextResponse.redirect).not.toHaveBeenCalled(); - expect(MockedNextResponse.next).toHaveBeenCalledTimes(2); + 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' + ); }); }); }); From c8c333943150387df02cea8f793e48379a036664 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 21 Jul 2023 15:27:17 +0200 Subject: [PATCH 04/37] No umlauts --- .../test/middleware/middleware.test.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/next-intl/test/middleware/middleware.test.tsx b/packages/next-intl/test/middleware/middleware.test.tsx index 66b74928f..758fd8e40 100644 --- a/packages/next-intl/test/middleware/middleware.test.tsx +++ b/packages/next-intl/test/middleware/middleware.test.tsx @@ -228,7 +228,7 @@ describe('prefix-based routing', () => { home: '/', about: { en: '/about', - de: '/über' + de: '/ueber' }, users: { en: '/users', @@ -287,7 +287,7 @@ describe('prefix-based routing', () => { }); it('serves requests for a non-default locale at nested paths', () => { - middlewareWithPathnames(createMockRequest('/de/über', 'de')); + middlewareWithPathnames(createMockRequest('/de/ueber', 'de')); middlewareWithPathnames(createMockRequest('/de/benutzer', 'de')); middlewareWithPathnames(createMockRequest('/de/benutzer/1', 'de')); middlewareWithPathnames( @@ -311,7 +311,7 @@ describe('prefix-based routing', () => { }); it('redirects a request for a localized route that is not associated with the requested locale', () => { - middlewareWithPathnames(createMockRequest('/über', 'en')); + middlewareWithPathnames(createMockRequest('/ueber', 'en')); expect(MockedNextResponse.next).not.toHaveBeenCalled(); expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); expect(MockedNextResponse.redirect).toHaveBeenCalledTimes(1); @@ -328,7 +328,7 @@ describe('prefix-based routing', () => { expect(MockedNextResponse.next).not.toHaveBeenCalled(); expect(MockedNextResponse.redirect).toHaveBeenCalledTimes(2); expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( - 'http://localhost:3000/de/%C3%BCber' + 'http://localhost:3000/de/ueber' ); expect(MockedNextResponse.redirect.mock.calls[1][0].toString()).toBe( 'http://localhost:3000/de/benutzer/2' @@ -342,7 +342,7 @@ describe('prefix-based routing', () => { expect(MockedNextResponse.next).not.toHaveBeenCalled(); expect(MockedNextResponse.redirect).toHaveBeenCalledTimes(2); expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( - 'http://localhost:3000/de/%C3%BCber' + 'http://localhost:3000/de/ueber' ); expect(MockedNextResponse.redirect.mock.calls[1][0].toString()).toBe( 'http://localhost:3000/de/benutzer/2' @@ -448,7 +448,7 @@ describe('prefix-based routing', () => { home: '/', about: { en: '/about', - de: '/über' + de: '/ueber' }, users: { en: '/users', @@ -493,7 +493,7 @@ describe('prefix-based routing', () => { }); it('serves requests for a non-default locale at nested paths', () => { - middlewareWithPathnames(createMockRequest('/de/über', 'de')); + middlewareWithPathnames(createMockRequest('/de/ueber', 'de')); middlewareWithPathnames(createMockRequest('/de/benutzer', 'de')); middlewareWithPathnames(createMockRequest('/de/benutzer/1', 'de')); middlewareWithPathnames( @@ -518,7 +518,7 @@ describe('prefix-based routing', () => { 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/über', 'en')); + middlewareWithPathnames(createMockRequest('/en/ueber', 'en')); middlewareWithPathnames(createMockRequest('/en/benutzer/12', 'en')); expect(MockedNextResponse.next).not.toHaveBeenCalled(); expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); From 2c259350555ed8656be505fc9db9afc85e0c24e7 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 26 Jul 2023 12:41:08 +0200 Subject: [PATCH 05/37] Fix tests --- packages/next-intl/test/middleware/middleware.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/next-intl/test/middleware/middleware.test.tsx b/packages/next-intl/test/middleware/middleware.test.tsx index 97b0b02b6..eddbd9e7a 100644 --- a/packages/next-intl/test/middleware/middleware.test.tsx +++ b/packages/next-intl/test/middleware/middleware.test.tsx @@ -1,3 +1,5 @@ +// @vitest-environment edge-runtime + import {RequestCookies} from 'next/dist/compiled/@edge-runtime/cookies'; import {NextRequest, NextResponse} from 'next/server'; import {it, describe, vi, beforeEach, expect, Mock} from 'vitest'; From 96bdb5ddfafb8a9cf3f8df6a097091218e78c0c4 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Wed, 26 Jul 2023 13:11:42 +0200 Subject: [PATCH 06/37] No const type argument for now --- .../next-intl/src/middleware/middleware.tsx | 2 +- packages/next-intl/src/server/index.tsx | 2 +- pnpm-lock.yaml | 129 +++++++++++------- 3 files changed, 79 insertions(+), 54 deletions(-) diff --git a/packages/next-intl/src/middleware/middleware.tsx b/packages/next-intl/src/middleware/middleware.tsx index 63657f148..8726f7547 100644 --- a/packages/next-intl/src/middleware/middleware.tsx +++ b/packages/next-intl/src/middleware/middleware.tsx @@ -82,7 +82,7 @@ function receiveConfig( } // TODO: eslint-config-molindo needs an upgrade of @typescript-eslint/parser -export default function createMiddleware( +export default function createMiddleware( config: MiddlewareConfig ) { const configWithDefaults = receiveConfig(config); diff --git a/packages/next-intl/src/server/index.tsx b/packages/next-intl/src/server/index.tsx index aece75dc3..638753445 100644 --- a/packages/next-intl/src/server/index.tsx +++ b/packages/next-intl/src/server/index.tsx @@ -9,7 +9,7 @@ import MiddlewareConfig, { let hasWarnedForMiddlewareImport = false; /** @deprecated Should be imported as `import createMiddleware from 'next-intl/middleware', not from `next-intl/server`. */ -export function createIntlMiddleware( +export function createIntlMiddleware( config: MiddlewareConfig ) { if (!hasWarnedForMiddlewareImport) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8724e289d..4acd07f18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,7 +49,7 @@ importers: version: 18.2.0(react@18.2.0) tailwindcss: specifier: ^3.2.4 - version: 3.2.4(postcss@8.4.26) + version: 3.2.4(postcss@8.4.27) devDependencies: '@types/node': specifier: ^20.1.2 @@ -59,7 +59,7 @@ importers: version: 18.2.5 autoprefixer: specifier: ^10.4.0 - version: 10.4.0(postcss@8.4.26) + version: 10.4.0(postcss@8.4.27) eslint: specifier: ^8.39.0 version: 8.39.0 @@ -71,7 +71,7 @@ importers: version: 13.4.0(eslint@8.39.0)(typescript@5.0.4) next-sitemap: specifier: ^4.0.7 - version: 4.0.7(@next/env@13.4.11)(next@13.4.7) + version: 4.0.7(@next/env@13.4.12)(next@13.4.7) prettier-plugin-tailwindcss: specifier: ^0.2.3 version: 0.2.3(prettier@3.0.0) @@ -555,7 +555,7 @@ importers: version: 18.2.0(react@18.2.0) rollup-plugin-preserve-directives: specifier: 0.2.0 - version: 0.2.0(rollup@3.21.4) + version: 0.2.0(rollup@3.26.3) size-limit: specifier: ^8.2.6 version: 8.2.6 @@ -955,10 +955,10 @@ packages: - supports-color dev: true - /@babel/helper-define-polyfill-provider@0.4.1(@babel/core@7.21.8): - resolution: {integrity: sha512-kX4oXixDxG197yhX+J3Wp+NpL2wuCFjWQAr6yX2jtCnflK9ulMI51ULFGIrWiX1jGfvAxdHp+XQCcP2bZGPs9A==} + /@babel/helper-define-polyfill-provider@0.4.2(@babel/core@7.21.8): + resolution: {integrity: sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==} peerDependencies: - '@babel/core': ^7.4.0-0 + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: '@babel/core': 7.21.8 '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.21.8) @@ -3609,11 +3609,11 @@ packages: '@babel/plugin-transform-unicode-property-regex': 7.22.5(@babel/core@7.21.8) '@babel/plugin-transform-unicode-regex': 7.22.5(@babel/core@7.21.8) '@babel/plugin-transform-unicode-sets-regex': 7.22.5(@babel/core@7.21.8) - '@babel/preset-modules': 0.1.5(@babel/core@7.21.8) + '@babel/preset-modules': 0.1.6(@babel/core@7.21.8) '@babel/types': 7.22.5 - babel-plugin-polyfill-corejs2: 0.4.4(@babel/core@7.21.8) - babel-plugin-polyfill-corejs3: 0.8.2(@babel/core@7.21.8) - babel-plugin-polyfill-regenerator: 0.5.1(@babel/core@7.21.8) + babel-plugin-polyfill-corejs2: 0.4.5(@babel/core@7.21.8) + babel-plugin-polyfill-corejs3: 0.8.3(@babel/core@7.21.8) + babel-plugin-polyfill-regenerator: 0.5.2(@babel/core@7.21.8) core-js-compat: 3.31.1 semver: 6.3.1 transitivePeerDependencies: @@ -3657,6 +3657,19 @@ packages: esutils: 2.0.3 dev: true + /@babel/preset-modules@0.1.6(@babel/core@7.21.8): + resolution: {integrity: sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.21.8) + '@babel/types': 7.22.5 + esutils: 2.0.3 + dev: false + /@babel/preset-typescript@7.21.5(@babel/core@7.21.8): resolution: {integrity: sha512-iqe3sETat5EOrORXiQ6rWfoOg2y68Cs75B9wNxdPW4kixJxh7aXQE1KPdWLDniC24T/6dSnguF33W9j/ZZQcmA==} engines: {node: '>=6.9.0'} @@ -5476,8 +5489,8 @@ packages: '@napi-rs/simple-git-win32-x64-msvc': 0.1.8 dev: false - /@next/env@13.4.11: - resolution: {integrity: sha512-BbPLOkfFFca+OpwSmGhNrl56B3ZzOFS4eJ85SpGTDpjlXoz7XFfS3MMOfjUCTuW1laTrQjm98I9Q30tsYpr8ew==} + /@next/env@13.4.12: + resolution: {integrity: sha512-RmHanbV21saP/6OEPBJ7yJMuys68cIf8OBBWd7+uj40LdpmswVAwe1uzeuFyUsd6SfeITWT3XnQfn6wULeKwDQ==} dev: true /@next/env@13.4.7: @@ -5561,11 +5574,6 @@ packages: requiresBuild: true optional: true - /@nicolo-ribaudo/semver-v6@6.3.3: - resolution: {integrity: sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==} - hasBin: true - dev: false - /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -6737,7 +6745,7 @@ packages: lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 3.2.4(postcss@8.4.26) + tailwindcss: 3.2.4(postcss@8.4.27) dev: false /@testing-library/dom@8.20.0: @@ -8387,7 +8395,7 @@ packages: postcss-value-parser: 4.2.0 dev: true - /autoprefixer@10.4.0(postcss@8.4.26): + /autoprefixer@10.4.0(postcss@8.4.27): resolution: {integrity: sha512-7FdJ1ONtwzV1G43GDD0kpVMn/qbiNqyOPMFTX5nRffI+7vgWoFEc6DcXOxHJxrWNDXrZh18eDsZjvZGUljSRGA==} engines: {node: ^10 || ^12 || >=14} hasBin: true @@ -8399,7 +8407,7 @@ packages: fraction.js: 4.2.0 normalize-range: 0.1.2 picocolors: 1.0.0 - postcss: 8.4.26 + postcss: 8.4.27 postcss-value-parser: 4.2.0 dev: true @@ -8596,15 +8604,15 @@ packages: - supports-color dev: true - /babel-plugin-polyfill-corejs2@0.4.4(@babel/core@7.21.8): - resolution: {integrity: sha512-9WeK9snM1BfxB38goUEv2FLnA6ja07UMfazFHzCXUb3NyDZAwfXvQiURQ6guTTMeHcOsdknULm1PDhs4uWtKyA==} + /babel-plugin-polyfill-corejs2@0.4.5(@babel/core@7.21.8): + resolution: {integrity: sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: '@babel/compat-data': 7.22.9 '@babel/core': 7.21.8 - '@babel/helper-define-polyfill-provider': 0.4.1(@babel/core@7.21.8) - '@nicolo-ribaudo/semver-v6': 6.3.3 + '@babel/helper-define-polyfill-provider': 0.4.2(@babel/core@7.21.8) + semver: 6.3.1 transitivePeerDependencies: - supports-color dev: false @@ -8644,13 +8652,13 @@ packages: - supports-color dev: true - /babel-plugin-polyfill-corejs3@0.8.2(@babel/core@7.21.8): - resolution: {integrity: sha512-Cid+Jv1BrY9ReW9lIfNlNpsI53N+FN7gE+f73zLAUbr9C52W4gKLWSByx47pfDJsEysojKArqOtOKZSVIIUTuQ==} + /babel-plugin-polyfill-corejs3@0.8.3(@babel/core@7.21.8): + resolution: {integrity: sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: '@babel/core': 7.21.8 - '@babel/helper-define-polyfill-provider': 0.4.1(@babel/core@7.21.8) + '@babel/helper-define-polyfill-provider': 0.4.2(@babel/core@7.21.8) core-js-compat: 3.31.1 transitivePeerDependencies: - supports-color @@ -8699,13 +8707,13 @@ packages: - supports-color dev: true - /babel-plugin-polyfill-regenerator@0.5.1(@babel/core@7.21.8): - resolution: {integrity: sha512-L8OyySuI6OSQ5hFy9O+7zFjyr4WhAfRjLIOkhQGYl+emwJkd/S4XXT1JpfrgR1jrQ1NcGiOh+yAdGlF8pnC3Jw==} + /babel-plugin-polyfill-regenerator@0.5.2(@babel/core@7.21.8): + resolution: {integrity: sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: '@babel/core': 7.21.8 - '@babel/helper-define-polyfill-provider': 0.4.1(@babel/core@7.21.8) + '@babel/helper-define-polyfill-provider': 0.4.2(@babel/core@7.21.8) transitivePeerDependencies: - supports-color dev: false @@ -18832,7 +18840,7 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /next-sitemap@4.0.7(@next/env@13.4.11)(next@13.4.7): + /next-sitemap@4.0.7(@next/env@13.4.12)(next@13.4.7): resolution: {integrity: sha512-S2g5IwJeO0+ecmFq981fb+Mw9YWmntOuN/qTCxclSkUibOJ8qKIOye0vn6NEJ1S4tKhbY+MTYKgJpNdFZYxLoA==} engines: {node: '>=14.18'} hasBin: true @@ -18841,7 +18849,7 @@ packages: next: '*' dependencies: '@corex/deepmerge': 4.0.43 - '@next/env': 13.4.11 + '@next/env': 13.4.12 minimist: 1.2.8 next: 13.4.7(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) dev: true @@ -20400,13 +20408,13 @@ packages: resolve: 1.22.2 dev: false - /postcss-import@14.1.0(postcss@8.4.26): + /postcss-import@14.1.0(postcss@8.4.27): resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} engines: {node: '>=10.0.0'} peerDependencies: postcss: ^8.0.0 dependencies: - postcss: 8.4.26 + postcss: 8.4.27 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.2 @@ -20422,14 +20430,14 @@ packages: postcss: 8.4.23 dev: false - /postcss-js@4.0.1(postcss@8.4.26): + /postcss-js@4.0.1(postcss@8.4.27): resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} engines: {node: ^12 || ^14 || >= 16} peerDependencies: postcss: ^8.4.21 dependencies: camelcase-css: 2.0.1 - postcss: 8.4.26 + postcss: 8.4.27 dev: false /postcss-load-config@3.1.4(postcss@8.4.23): @@ -20449,7 +20457,7 @@ packages: yaml: 1.10.2 dev: false - /postcss-load-config@3.1.4(postcss@8.4.26): + /postcss-load-config@3.1.4(postcss@8.4.27): resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} engines: {node: '>= 10'} peerDependencies: @@ -20462,7 +20470,7 @@ packages: optional: true dependencies: lilconfig: 2.1.0 - postcss: 8.4.26 + postcss: 8.4.27 yaml: 1.10.2 dev: false @@ -20644,13 +20652,13 @@ packages: postcss-selector-parser: 6.0.12 dev: false - /postcss-nested@6.0.0(postcss@8.4.26): + /postcss-nested@6.0.0(postcss@8.4.27): resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==} engines: {node: '>=12.0'} peerDependencies: postcss: ^8.2.14 dependencies: - postcss: 8.4.26 + postcss: 8.4.27 postcss-selector-parser: 6.0.12 dev: false @@ -20860,6 +20868,15 @@ packages: nanoid: 3.3.6 picocolors: 1.0.0 source-map-js: 1.0.2 + dev: true + + /postcss@8.4.27: + resolution: {integrity: sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 /preact-render-to-string@5.2.6(preact@10.13.2): resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} @@ -22207,13 +22224,13 @@ packages: rollup-plugin-inject: 3.0.2 dev: true - /rollup-plugin-preserve-directives@0.2.0(rollup@3.21.4): + /rollup-plugin-preserve-directives@0.2.0(rollup@3.26.3): resolution: {integrity: sha512-KUwbBaFvD1zFIDNnOkR+u64sSod3m0l6q46/SzTOa4GTQ6hp6w0FRr2u7x99YkY9qhlna5panmTmuLWeJ/2KWw==} peerDependencies: rollup: 2.x || 3.x dependencies: magic-string: 0.30.0 - rollup: 3.21.4 + rollup: 3.26.3 dev: true /rollup-plugin-sourcemaps@0.6.3(@types/node@17.0.23)(rollup@2.79.1): @@ -22283,6 +22300,14 @@ packages: fsevents: 2.3.2 dev: true + /rollup@3.26.3: + resolution: {integrity: sha512-7Tin0C8l86TkpcMtXvQu6saWH93nhG3dGQ1/+l5V2TDMceTxO7kDiK6GzbfLWNNxqJXm591PcEZUozZm51ogwQ==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + /run-applescript@5.0.0: resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} engines: {node: '>=12'} @@ -23526,7 +23551,7 @@ packages: - ts-node dev: false - /tailwindcss@3.2.4(postcss@8.4.26): + /tailwindcss@3.2.4(postcss@8.4.27): resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==} engines: {node: '>=12.13.0'} hasBin: true @@ -23547,11 +23572,11 @@ packages: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.0 - postcss: 8.4.26 - postcss-import: 14.1.0(postcss@8.4.26) - postcss-js: 4.0.1(postcss@8.4.26) - postcss-load-config: 3.1.4(postcss@8.4.26) - postcss-nested: 6.0.0(postcss@8.4.26) + postcss: 8.4.27 + postcss-import: 14.1.0(postcss@8.4.27) + postcss-js: 4.0.1(postcss@8.4.27) + postcss-load-config: 3.1.4(postcss@8.4.27) + postcss-nested: 6.0.0(postcss@8.4.27) postcss-selector-parser: 6.0.12 postcss-value-parser: 4.2.0 quick-lru: 5.1.1 From 1513e0357d364fdaf19297ff08344436c7c789a6 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 28 Jul 2023 09:57:42 +0200 Subject: [PATCH 07/37] PROGRESS --- .../src/components/LocaleSwitcherSelect.tsx | 6 +- .../src/components/NavigationLink.tsx | 13 +- examples/example-next-13/src/i18n.ts | 12 ++ examples/example-next-13/src/middleware.ts | 11 +- examples/example-next-13/src/navigation.tsx | 21 +++ packages/next-intl/navigation.d.ts | 1 + packages/next-intl/package.json | 6 +- .../next-intl/src/client/useClientLocale.tsx | 1 + .../src/middleware/LocalizedPathnames.tsx | 6 +- .../middleware/NextIntlMiddlewareConfig.tsx | 14 +- .../getAlternateLinksHeaderValue.tsx | 2 +- .../next-intl/src/middleware/middleware.tsx | 2 +- .../src/middleware/resolveLocale.tsx | 2 +- packages/next-intl/src/middleware/utils.tsx | 3 +- .../next-intl/src/navigation.react-server.tsx | 1 + packages/next-intl/src/navigation.tsx | 1 + .../src/navigation/createNamedNavigation.tsx | 136 ++++++++++++++++++ packages/next-intl/src/navigation/index.tsx | 1 + .../react-server/createNamedNavigation.tsx | 78 ++++++++++ .../src/navigation/react-server/index.tsx | 1 + packages/next-intl/src/navigation/utils.tsx | 66 +++++++++ .../next-intl/src/server/baseRedirect.tsx | 12 ++ packages/next-intl/src/server/index.tsx | 5 +- .../src/server/react-client/index.tsx | 2 +- .../src/server/react-client/redirect.tsx | 12 +- packages/next-intl/src/server/redirect.tsx | 10 +- packages/next-intl/src/shared/redirect.tsx | 7 - packages/next-intl/src/shared/types.tsx | 22 +++ .../test/middleware/middleware.test.tsx | 25 ++-- 29 files changed, 412 insertions(+), 67 deletions(-) create mode 100644 examples/example-next-13/src/navigation.tsx create mode 100644 packages/next-intl/navigation.d.ts create mode 100644 packages/next-intl/src/navigation.react-server.tsx create mode 100644 packages/next-intl/src/navigation.tsx create mode 100644 packages/next-intl/src/navigation/createNamedNavigation.tsx create mode 100644 packages/next-intl/src/navigation/index.tsx create mode 100644 packages/next-intl/src/navigation/react-server/createNamedNavigation.tsx create mode 100644 packages/next-intl/src/navigation/react-server/index.tsx create mode 100644 packages/next-intl/src/navigation/utils.tsx create mode 100644 packages/next-intl/src/server/baseRedirect.tsx delete mode 100644 packages/next-intl/src/shared/redirect.tsx create mode 100644 packages/next-intl/src/shared/types.tsx diff --git a/examples/example-next-13/src/components/LocaleSwitcherSelect.tsx b/examples/example-next-13/src/components/LocaleSwitcherSelect.tsx index 317efe380..4c91488e5 100644 --- a/examples/example-next-13/src/components/LocaleSwitcherSelect.tsx +++ b/examples/example-next-13/src/components/LocaleSwitcherSelect.tsx @@ -1,8 +1,8 @@ 'use client'; import clsx from 'clsx'; -import {usePathname, useRouter} from 'next-intl/client'; import {ChangeEvent, ReactNode, useTransition} from 'react'; +import {useRouter, usePathname} from '../navigation'; type Props = { children: ReactNode; @@ -16,13 +16,13 @@ export default function LocaleSwitcherSelect({ label }: Props) { const router = useRouter(); - const pathname = usePathname(); const [isPending, startTransition] = useTransition(); + const namedPath = usePathname(); function onSelectChange(event: ChangeEvent) { const nextLocale = event.target.value; startTransition(() => { - router.replace(pathname, {locale: nextLocale}); + router.replace(namedPath, {locale: nextLocale}); }); } diff --git a/examples/example-next-13/src/components/NavigationLink.tsx b/examples/example-next-13/src/components/NavigationLink.tsx index 3716ff0ec..ed9f46d29 100644 --- a/examples/example-next-13/src/components/NavigationLink.tsx +++ b/examples/example-next-13/src/components/NavigationLink.tsx @@ -1,18 +1,15 @@ '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} from '../navigation'; -type Props = Omit, 'href'> & { - href: string; -}; +type Props = ComponentProps; export default function NavigationLink({href, ...rest}: Props) { - const pathname = usePathname(); - - // TODO: We need to consult the pathnames map here + const selectedLayoutSegment = useSelectedLayoutSegment(); + const pathname = selectedLayoutSegment ? `/${selectedLayoutSegment}` : '/'; const isActive = pathname === href; return ( diff --git a/examples/example-next-13/src/i18n.ts b/examples/example-next-13/src/i18n.ts index c45ddee14..af99022e0 100644 --- a/examples/example-next-13/src/i18n.ts +++ b/examples/example-next-13/src/i18n.ts @@ -1,5 +1,17 @@ import {getRequestConfig} from 'next-intl/server'; +export const locales = ['en', 'de'] as const; + +// is it good that export this here? do we need to worry about tree shaking to include server-only code? +// try in advanced example +export const pathnames = { + '/': '/', + '/about': { + en: '/about', + de: '/ueber' + } +} as const; + export default getRequestConfig(async ({locale}) => ({ 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 index 90d739f71..fdad3a20b 100644 --- a/examples/example-next-13/src/middleware.ts +++ b/examples/example-next-13/src/middleware.ts @@ -1,15 +1,10 @@ import createMiddleware from 'next-intl/middleware'; +import {pathnames, locales} from './i18n'; export default createMiddleware({ - locales: ['en', 'de'], defaultLocale: 'en', - pathnames: { - home: '/', - about: { - en: '/about', - de: '/ueber' - } - } + locales, + pathnames }); export const config = { diff --git a/examples/example-next-13/src/navigation.tsx b/examples/example-next-13/src/navigation.tsx new file mode 100644 index 000000000..f29436b85 --- /dev/null +++ b/examples/example-next-13/src/navigation.tsx @@ -0,0 +1,21 @@ +import {createNamedNavigation} from 'next-intl/navigation'; +import {locales, pathnames} from './i18n'; + +export const {Link, redirect, usePathname, useRouter} = createNamedNavigation({ + locales, + pathnames +}); + +// export const {Link, redirect, useRouter} = createDefaultNavigation({ +// locales, +// pathnames +// }); + +// this has some advantages: +// - no type overloading for redirect and router push +// - strong types for the locale and default locale +// - usePathname + +// can these apis be used in both server and client files? +// maybe we need to fork createNamedRouting into a react-server version +// that could work, because we have to separate bundles diff --git a/packages/next-intl/navigation.d.ts b/packages/next-intl/navigation.d.ts new file mode 100644 index 000000000..ab45eb662 --- /dev/null +++ b/packages/next-intl/navigation.d.ts @@ -0,0 +1 @@ +export * from './dist/navigation'; diff --git a/packages/next-intl/package.json b/packages/next-intl/package.json index 9175a1201..f4c4d0c04 100644 --- a/packages/next-intl/package.json +++ b/packages/next-intl/package.json @@ -11,7 +11,7 @@ "url": "https://github.com/amannn/next-intl" }, "scripts": { - "build": "dts build --entry src/index.tsx && dts build --entry src/client.tsx --entry src/index.react-server.tsx --entry src/link.react-server.tsx --entry src/link.tsx --entry src/middleware.tsx --entry src/plugin.tsx --entry src/server.react-server.tsx --entry src/server.tsx --entry src/config.tsx --noClean", + "build": "dts build --entry src/index.tsx && dts build --entry src/client.tsx --entry src/index.react-server.tsx --entry src/link.react-server.tsx --entry src/link.tsx --entry src/middleware.tsx --entry src/navigation.tsx --entry src/navigation.react-server.tsx --entry src/plugin.tsx --entry src/server.react-server.tsx --entry src/server.tsx --entry src/config.tsx --noClean", "test": "TZ=Europe/Berlin vitest", "lint": "eslint src test && tsc --noEmit", "prepublishOnly": "CI=true turbo test && turbo lint && turbo build && cp ../../README.md .", @@ -46,6 +46,10 @@ "types": "./middleware.d.ts", "default": "./dist/middleware.js" }, + "./navigation": { + "react-server": "./dist/navigation.react-server.esm.js", + "default": "./dist/navigation.js" + }, "./withNextIntl": "./withNextIntl.js", "./plugin": "./dist/plugin.js" }, diff --git a/packages/next-intl/src/client/useClientLocale.tsx b/packages/next-intl/src/client/useClientLocale.tsx index a4e7036d2..2c3288f21 100644 --- a/packages/next-intl/src/client/useClientLocale.tsx +++ b/packages/next-intl/src/client/useClientLocale.tsx @@ -2,6 +2,7 @@ import {useParams} from 'next/navigation'; import {useLocale} from 'use-intl'; import {LOCALE_SEGMENT_NAME} from '../shared/constants'; +// TODO: Potentially we could export this instead of useLocale from use-intl? export default function useClientLocale(): string { let locale; diff --git a/packages/next-intl/src/middleware/LocalizedPathnames.tsx b/packages/next-intl/src/middleware/LocalizedPathnames.tsx index 5701bb4f2..82ddfaddf 100644 --- a/packages/next-intl/src/middleware/LocalizedPathnames.tsx +++ b/packages/next-intl/src/middleware/LocalizedPathnames.tsx @@ -1,8 +1,6 @@ import {NextRequest} from 'next/server'; -import { - AllLocales, - MiddlewareConfigWithDefaults -} from './NextIntlMiddlewareConfig'; +import {AllLocales} from '../shared/types'; +import {MiddlewareConfigWithDefaults} from './NextIntlMiddlewareConfig'; import { formatPathname, getKnownLocaleFromPathname, diff --git a/packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx b/packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx index d2d1af784..5119634e8 100644 --- a/packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx +++ b/packages/next-intl/src/middleware/NextIntlMiddlewareConfig.tsx @@ -1,3 +1,5 @@ +import {AllLocales, Pathnames} from '../shared/types'; + type RoutingConfigPrefix = { type: 'prefix'; @@ -15,9 +17,6 @@ type RoutingConfigDomain = { type LocalePrefix = 'as-needed' | 'always' | 'never'; -type Locale = string; -export type AllLocales = ReadonlyArray; - type RoutingBaseConfig = { /** A list of all locales that are supported. */ locales: Locales; @@ -44,11 +43,6 @@ export type DomainConfig = Omit< locale?: string; }; -export type Pathnames = Record< - string, - {[Key in Locales[number]]: string} | string ->; - // TODO: Default or not? type MiddlewareConfig = RoutingBaseConfig & { @@ -66,6 +60,10 @@ type MiddlewareConfig = /** TODO */ 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 = diff --git a/packages/next-intl/src/middleware/getAlternateLinksHeaderValue.tsx b/packages/next-intl/src/middleware/getAlternateLinksHeaderValue.tsx index 25d5196d5..d69c97b7b 100644 --- a/packages/next-intl/src/middleware/getAlternateLinksHeaderValue.tsx +++ b/packages/next-intl/src/middleware/getAlternateLinksHeaderValue.tsx @@ -1,6 +1,6 @@ import {NextRequest} from 'next/server'; +import {AllLocales} from '../shared/types'; import MiddlewareConfig, { - AllLocales, MiddlewareConfigWithDefaults } from './NextIntlMiddlewareConfig'; import {isLocaleSupportedOnDomain} from './utils'; diff --git a/packages/next-intl/src/middleware/middleware.tsx b/packages/next-intl/src/middleware/middleware.tsx index 8726f7547..1b00a20a3 100644 --- a/packages/next-intl/src/middleware/middleware.tsx +++ b/packages/next-intl/src/middleware/middleware.tsx @@ -1,11 +1,11 @@ import {NextRequest, NextResponse} from 'next/server'; import {COOKIE_LOCALE_NAME, HEADER_LOCALE_NAME} from '../shared/constants'; +import {AllLocales} from '../shared/types'; import { getLocalizedRewritePathname, getLocalizedRedirectPathname } from './LocalizedPathnames'; import MiddlewareConfig, { - AllLocales, MiddlewareConfigWithDefaults } from './NextIntlMiddlewareConfig'; import getAlternateLinksHeaderValue from './getAlternateLinksHeaderValue'; diff --git a/packages/next-intl/src/middleware/resolveLocale.tsx b/packages/next-intl/src/middleware/resolveLocale.tsx index aee785928..68a862898 100644 --- a/packages/next-intl/src/middleware/resolveLocale.tsx +++ b/packages/next-intl/src/middleware/resolveLocale.tsx @@ -2,8 +2,8 @@ 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 { - AllLocales, DomainConfig, MiddlewareConfigWithDefaults } from './NextIntlMiddlewareConfig'; diff --git a/packages/next-intl/src/middleware/utils.tsx b/packages/next-intl/src/middleware/utils.tsx index a8ffa0afe..41b913db8 100644 --- a/packages/next-intl/src/middleware/utils.tsx +++ b/packages/next-intl/src/middleware/utils.tsx @@ -1,5 +1,6 @@ import {pathToRegexp, match, compile} from 'path-to-regexp'; -import {AllLocales, DomainConfig} from './NextIntlMiddlewareConfig'; +import {AllLocales} from '../shared/types'; +import {DomainConfig} from './NextIntlMiddlewareConfig'; export function getLocaleFromPathname(pathname: string) { return pathname.split('/')[1]; 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/createNamedNavigation.tsx b/packages/next-intl/src/navigation/createNamedNavigation.tsx new file mode 100644 index 000000000..6999be595 --- /dev/null +++ b/packages/next-intl/src/navigation/createNamedNavigation.tsx @@ -0,0 +1,136 @@ +import React, {ComponentProps, forwardRef} from 'react'; +import { + useRouter as useBaseRouter, + usePathname as useBasePathname +} from '../client'; +import useClientLocale from '../client/useClientLocale'; +import BaseLink from '../link'; +import baseRedirect from '../server/react-client/redirect'; +import {AllLocales, ParametersExceptFirst, Pathnames} from '../shared/types'; +import {Params, compileNamedRoute, getNamedRoute} from './utils'; + +export default function createNamedNavigation({ + locales, + pathnames +}: { + locales: Locales; + pathnames: Pathnames; +}) { + function useLocale() { + return useClientLocale() as (typeof locales)[number]; + } + + const Link = forwardRef( + ( + { + href, + locale, + params, + ...rest + }: Omit, 'href' | 'name'> & { + href: keyof Pathnames; + params?: Params; + locale?: Locales[number]; + }, + ref: ComponentProps['ref'] + ) => { + const defaultLocale = useLocale(); + const finalLocale = locale || defaultLocale; + + return ( + + ); + } + ); + Link.displayName = 'Link'; + + type NameOrNameWithParams = + | keyof Pathnames + | { + href: keyof Pathnames; + params?: Params; + }; + + function normalizeNameOrNameWithParams( + nameOrNameWithParams: NameOrNameWithParams + ) { + return typeof nameOrNameWithParams === 'string' + ? {href: nameOrNameWithParams, params: undefined} + : nameOrNameWithParams; + } + + function redirect( + nameOrNameWithParams: NameOrNameWithParams, + ...args: ParametersExceptFirst + ) { + // eslint-disable-next-line react-hooks/rules-of-hooks -- Reading from context conditionally is fine + const locale = useLocale(); + const href = compileNamedRoute({ + ...normalizeNameOrNameWithParams(nameOrNameWithParams), + locale, + pathnames + }); + return baseRedirect(href, ...args); + } + + function useRouter() { + const baseRouter = useBaseRouter(); + const locale = useLocale(); + + return { + ...baseRouter, + push( + nameOrNameWithParams: NameOrNameWithParams, + ...args: ParametersExceptFirst + ) { + const href = compileNamedRoute({ + ...normalizeNameOrNameWithParams(nameOrNameWithParams), + locale, + pathnames + }); + return baseRouter.push(href, ...args); + }, + + replace( + nameOrNameWithParams: NameOrNameWithParams, + ...args: ParametersExceptFirst + ) { + const href = compileNamedRoute({ + ...normalizeNameOrNameWithParams(nameOrNameWithParams), + locale, + pathnames + }); + return baseRouter.replace(href, ...args); + }, + + prefetch( + nameOrNameWithParams: NameOrNameWithParams, + ...args: ParametersExceptFirst + ) { + const href = compileNamedRoute({ + ...normalizeNameOrNameWithParams(nameOrNameWithParams), + locale, + pathnames + }); + return baseRouter.prefetch(href, ...args); + } + }; + } + + function usePathname() { + const pathname = useBasePathname(); + const locale = useLocale(); + return getNamedRoute({pathname, locale, pathnames}); + } + + return {Link, redirect, usePathname, useRouter}; +} diff --git a/packages/next-intl/src/navigation/index.tsx b/packages/next-intl/src/navigation/index.tsx new file mode 100644 index 000000000..cd561943e --- /dev/null +++ b/packages/next-intl/src/navigation/index.tsx @@ -0,0 +1 @@ +export {default as createNamedNavigation} from './createNamedNavigation'; diff --git a/packages/next-intl/src/navigation/react-server/createNamedNavigation.tsx b/packages/next-intl/src/navigation/react-server/createNamedNavigation.tsx new file mode 100644 index 000000000..e3f0fd427 --- /dev/null +++ b/packages/next-intl/src/navigation/react-server/createNamedNavigation.tsx @@ -0,0 +1,78 @@ +import React, {ComponentProps} from 'react'; +import BaseLink from '../../link/react-server'; +import {redirect as baseRedirect} from '../../server'; +import getLocaleFromHeader from '../../server/getLocaleFromHeader'; +import {AllLocales, ParametersExceptFirst, Pathnames} from '../../shared/types'; +import {Params, compileNamedRoute} from '../utils'; + +export default function createNamedNavigation({ + locales, + pathnames +}: { + locales: Locales; + pathnames: Pathnames; +}) { + function Link({ + href, + locale, + params, + ...rest + }: Omit, 'href' | 'name'> & { + href: keyof Pathnames; + params?: Params; + locale?: Locales[number]; + }) { + const defaultLocale = getLocaleFromHeader() as (typeof locales)[number]; + const finalLocale = locale || defaultLocale; + + return ( + + ); + } + + function redirect( + nameOrNameWithParams: + | keyof Pathnames + | { + name: keyof Pathnames; + params?: Params; + }, + ...args: ParametersExceptFirst + ) { + const {name, params} = + typeof nameOrNameWithParams === 'string' + ? {name: nameOrNameWithParams, params: undefined} + : nameOrNameWithParams; + + const locale = getLocaleFromHeader(); + const href = compileNamedRoute({ + locale, + href: name, + params, + pathnames + }); + + return baseRedirect(href, ...args); + } + + function notSupported(message: string) { + 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, + 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..cd561943e --- /dev/null +++ b/packages/next-intl/src/navigation/react-server/index.tsx @@ -0,0 +1 @@ +export {default as createNamedNavigation} from './createNamedNavigation'; diff --git a/packages/next-intl/src/navigation/utils.tsx b/packages/next-intl/src/navigation/utils.tsx new file mode 100644 index 000000000..43f803469 --- /dev/null +++ b/packages/next-intl/src/navigation/utils.tsx @@ -0,0 +1,66 @@ +import {AllLocales, Pathnames} from '../shared/types'; + +// TODO: Can we type this? +export type Params = Record; + +export function compileNamedRoute({ + href: name, + locale, + params, + pathnames +}: { + locale: Locales[number]; + href: keyof Pathnames; + params?: Params; + pathnames: Pathnames; +}) { + const namedPath = pathnames[name]; + + if (!namedPath) { + throw new Error( + `No named route found for "${name}". Available routes: ${Object.keys( + pathnames + ).join(', ')}` + ); + } + + const href = typeof namedPath === 'string' ? namedPath : namedPath[locale]; + if (params) { + // Object.keys(params).forEach((param) => { + // href = href.replace( + // new RegExp(':' + param, 'g'), + // (params as any)[param] + // ); + // }); + } + + return href; +} + +export function getNamedRoute({ + locale, + pathname, + pathnames +}: { + locale: Locales[number]; + pathname: string; + pathnames: Pathnames; +}) { + // TODO: Consider params + const routeName = Object.entries(pathnames).find( + ([, routePath]) => + (typeof routePath !== 'string' ? routePath[locale] : routePath) === + pathname + )?.[0]; + + if (!routeName) { + throw new Error( + `No named route found for "${pathname}". Available routes: ${Object.keys( + pathnames + ).join(', ')}` + ); + } + + // TODO: Fix typing with const assertion + return routeName as keyof Pathnames; +} diff --git a/packages/next-intl/src/server/baseRedirect.tsx b/packages/next-intl/src/server/baseRedirect.tsx new file mode 100644 index 000000000..dd344bb95 --- /dev/null +++ b/packages/next-intl/src/server/baseRedirect.tsx @@ -0,0 +1,12 @@ +import {redirect as nextRedirect} from 'next/navigation'; +import {ParametersExceptFirst} from '../shared/types'; +import {localizePathname} from '../shared/utils'; + +export default function baseRedirect( + pathname: string, + locale: string, + ...args: ParametersExceptFirst +) { + const localizedPathname = localizePathname(locale, pathname); + return nextRedirect(localizedPathname, ...args); +} diff --git a/packages/next-intl/src/server/index.tsx b/packages/next-intl/src/server/index.tsx index 638753445..b41aef1f8 100644 --- a/packages/next-intl/src/server/index.tsx +++ b/packages/next-intl/src/server/index.tsx @@ -3,9 +3,8 @@ */ import createMiddleware_ from '../middleware'; -import MiddlewareConfig, { - AllLocales -} from '../middleware/NextIntlMiddlewareConfig'; +import MiddlewareConfig from '../middleware/NextIntlMiddlewareConfig'; +import {AllLocales} from '../shared/types'; let hasWarnedForMiddlewareImport = false; /** @deprecated Should be imported as `import createMiddleware from 'next-intl/middleware', not from `next-intl/server`. */ diff --git a/packages/next-intl/src/server/react-client/index.tsx b/packages/next-intl/src/server/react-client/index.tsx index 05d20ed29..0a0c8f759 100644 --- a/packages/next-intl/src/server/react-client/index.tsx +++ b/packages/next-intl/src/server/react-client/index.tsx @@ -19,7 +19,7 @@ function notSupported(name: string) { // Must match `../index.tsx` // prettier-ignore -export const getRequestConfig = notSupported('getRequestConfig') as unknown as typeof getRequestConfig_type; +export const getRequestConfig = (() => notSupported('getRequestConfig')) as unknown as typeof getRequestConfig_type; // prettier-ignore /** @deprecated Is called `getFormatter` now. */ export const getIntl = notSupported('getIntl') as unknown as typeof getIntl_type; diff --git a/packages/next-intl/src/server/react-client/redirect.tsx b/packages/next-intl/src/server/react-client/redirect.tsx index f7aa069eb..7c56944ad 100644 --- a/packages/next-intl/src/server/react-client/redirect.tsx +++ b/packages/next-intl/src/server/react-client/redirect.tsx @@ -1,8 +1,12 @@ import useClientLocale from '../../client/useClientLocale'; -import baseRedirect from '../../shared/redirect'; +import {ParametersExceptFirstTwo} from '../../shared/types'; +import baseRedirect from '../baseRedirect'; -export default function redirect(pathname: string) { - // eslint-disable-next-line react-hooks/rules-of-hooks +export default function redirect( + pathname: string, + ...args: ParametersExceptFirstTwo +) { + // eslint-disable-next-line react-hooks/rules-of-hooks -- Reading from context conditionally is fine const locale = useClientLocale(); - return baseRedirect(pathname, locale); + return baseRedirect(pathname, locale, ...args); } diff --git a/packages/next-intl/src/server/redirect.tsx b/packages/next-intl/src/server/redirect.tsx index 487ba71c0..ca6d434ea 100644 --- a/packages/next-intl/src/server/redirect.tsx +++ b/packages/next-intl/src/server/redirect.tsx @@ -1,7 +1,11 @@ -import baseRedirect from '../shared/redirect'; +import {ParametersExceptFirstTwo} from '../shared/types'; +import baseRedirect from './baseRedirect'; import getLocaleFromHeader from './getLocaleFromHeader'; -export default function redirect(pathname: string) { +export default function redirect( + pathname: string, + ...args: ParametersExceptFirstTwo +) { const locale = getLocaleFromHeader(); - return baseRedirect(pathname, locale); + return baseRedirect(pathname, locale, ...args); } diff --git a/packages/next-intl/src/shared/redirect.tsx b/packages/next-intl/src/shared/redirect.tsx deleted file mode 100644 index 563e3627c..000000000 --- a/packages/next-intl/src/shared/redirect.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import {redirect as nextRedirect} from 'next/navigation'; -import {localizePathname} from '../shared/utils'; - -export default function redirect(pathname: string, locale: string) { - const localizedPathname = localizePathname(locale, pathname); - return nextRedirect(localizedPathname); -} diff --git a/packages/next-intl/src/shared/types.tsx b/packages/next-intl/src/shared/types.tsx new file mode 100644 index 000000000..672173427 --- /dev/null +++ b/packages/next-intl/src/shared/types.tsx @@ -0,0 +1,22 @@ +export type Locale = string; +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/test/middleware/middleware.test.tsx b/packages/next-intl/test/middleware/middleware.test.tsx index eddbd9e7a..423c240f3 100644 --- a/packages/next-intl/test/middleware/middleware.test.tsx +++ b/packages/next-intl/test/middleware/middleware.test.tsx @@ -4,7 +4,6 @@ import {RequestCookies} from 'next/dist/compiled/@edge-runtime/cookies'; import {NextRequest, NextResponse} from 'next/server'; import {it, describe, vi, beforeEach, expect, Mock} from 'vitest'; import createIntlMiddleware from '../../src/middleware'; -import {DomainConfig} from '../../src/middleware/NextIntlMiddlewareConfig'; import {COOKIE_LOCALE_NAME} from '../../src/shared/constants'; vi.mock('next/server', () => { @@ -1051,21 +1050,21 @@ describe('deprecated domain config', () => { }); it('accepts deprecated config with `domain.locale`', () => { - const domains = [ - { - locale: 'en', - domain: 'en.example.com' - }, - { - locale: 'de', - domain: 'de.example.com' - } - ] as Array; - const middleware = createIntlMiddleware({ defaultLocale: 'en', locales: ['en', 'de'], - domains + domains: [ + // @ts-expect-error Deprecated config + { + locale: 'en', + domain: 'en.example.com' + }, + // @ts-expect-error Deprecated config + { + locale: 'de', + domain: 'de.example.com' + } + ] }); middleware(createMockRequest('/', 'en', 'http://en.example.com')); From 88e908cac2770179a683c6eb3d26314c7aa789d0 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 28 Jul 2023 14:39:44 +0200 Subject: [PATCH 08/37] Migrate advanced example --- .../example-next-13-advanced/messages/de.json | 6 +- .../example-next-13-advanced/messages/en.json | 6 +- .../example-next-13-advanced/messages/es.json | 6 +- .../example-next-13-advanced/next.config.js | 15 +- .../src/app/[locale]/client/ClientContent.tsx | 2 +- .../src/app/[locale]/client/redirect/page.tsx | 2 +- .../src/app/[locale]/layout.tsx | 14 +- .../[locale]/nested/UnlocalizedPathname.tsx | 7 + .../src/app/[locale]/nested/page.tsx | 2 + .../app/[locale]/news/[articleId]/page.tsx | 12 ++ .../src/app/[locale]/page.tsx | 2 +- .../src/app/[locale]/redirect/page.tsx | 2 +- .../src/components/ClientLink.tsx | 2 +- .../ClientRouterWithoutProvider.tsx | 2 +- .../src/components/LocaleSwitcher.tsx | 2 +- .../src/components/Navigation.tsx | 3 + .../src/components/NavigationLink.tsx | 21 ++- .../src/components/PageLayout.tsx | 16 +- .../example-next-13-advanced/src/config.tsx | 20 +++ .../src/middleware.ts | 6 +- .../src/navigation.tsx | 8 + .../tests/main.spec.ts | 37 ++++- .../src/components/NavigationLink.tsx | 2 +- examples/example-next-13/src/i18n.ts | 3 +- examples/example-next-13/src/navigation.tsx | 11 +- packages/next-intl/package.json | 1 - .../src/middleware/LocalizedPathnames.tsx | 42 +++-- packages/next-intl/src/middleware/utils.tsx | 43 +++-- ...=> createLocalizedPathnamesNavigation.tsx} | 45 +++--- packages/next-intl/src/navigation/index.tsx | 3 +- ...=> createLocalizedPathnamesNavigation.tsx} | 27 ++-- .../src/navigation/react-server/index.tsx | 2 +- packages/next-intl/src/navigation/utils.tsx | 77 ++++++--- packages/next-intl/src/shared/types.tsx | 13 ++ packages/next-intl/test/link/Link.test.tsx | 4 +- .../test/middleware/middleware.test.tsx | 36 ++--- .../next-intl/test/middleware/utils.test.tsx | 149 ++++++++++++++++++ ...reateLocalizedPathnamesNavigation.test.tsx | 69 ++++++++ pnpm-lock.yaml | 122 +++++++++----- 39 files changed, 626 insertions(+), 216 deletions(-) create mode 100644 examples/example-next-13-advanced/src/app/[locale]/nested/UnlocalizedPathname.tsx create mode 100644 examples/example-next-13-advanced/src/app/[locale]/news/[articleId]/page.tsx create mode 100644 examples/example-next-13-advanced/src/config.tsx create mode 100644 examples/example-next-13-advanced/src/navigation.tsx rename packages/next-intl/src/navigation/{createNamedNavigation.tsx => createLocalizedPathnamesNavigation.tsx} (74%) rename packages/next-intl/src/navigation/react-server/{createNamedNavigation.tsx => createLocalizedPathnamesNavigation.tsx} (71%) create mode 100644 packages/next-intl/test/middleware/utils.test.tsx create mode 100644 packages/next-intl/test/navigation/createLocalizedPathnamesNavigation.test.tsx diff --git a/examples/example-next-13-advanced/messages/de.json b/examples/example-next-13-advanced/messages/de.json index 668c7a7b3..86c12a2c3 100644 --- a/examples/example-next-13-advanced/messages/de.json +++ b/examples/example-next-13-advanced/messages/de.json @@ -20,7 +20,11 @@ "Navigation": { "home": "Start", "client": "Client-Seite", - "nested": "Verschachtelte Seite" + "nested": "Verschachtelte Seite", + "newsArticle": "News-Artikel #{articleId}" + }, + "NewsArticle": { + "title": "News-Artikel #{articleId}" }, "NotFound": { "title": "Diese Seite wurde nicht gefunden (404)" diff --git a/examples/example-next-13-advanced/messages/en.json b/examples/example-next-13-advanced/messages/en.json index 0af804dc1..daadd981e 100644 --- a/examples/example-next-13-advanced/messages/en.json +++ b/examples/example-next-13-advanced/messages/en.json @@ -20,7 +20,11 @@ "Navigation": { "home": "Home", "client": "Client page", - "nested": "Nested page" + "nested": "Nested page", + "newsArticle": "News article #{articleId}" + }, + "NewsArticle": { + "title": "News article #{articleId}" }, "NotFound": { "title": "This page was not found (404)" diff --git a/examples/example-next-13-advanced/messages/es.json b/examples/example-next-13-advanced/messages/es.json index 0dd77bb33..ed4c27356 100644 --- a/examples/example-next-13-advanced/messages/es.json +++ b/examples/example-next-13-advanced/messages/es.json @@ -20,7 +20,11 @@ "Navigation": { "home": "Inicio", "client": "Página del cliente", - "nested": "Página anidada" + "nested": "Página anidada", + "newsArticle": "Noticias #{articleId}" + }, + "NewsArticle": { + "title": "Noticias #{articleId}" }, "NotFound": { "title": "Esta página no se encontró (404)" diff --git a/examples/example-next-13-advanced/next.config.js b/examples/example-next-13-advanced/next.config.js index d12095e5b..d9797bdf4 100644 --- a/examples/example-next-13-advanced/next.config.js +++ b/examples/example-next-13-advanced/next.config.js @@ -1,16 +1,3 @@ const withNextIntl = require('next-intl/plugin')(); -module.exports = withNextIntl({ - rewrites() { - return [ - { - source: '/de/verschachtelt', - destination: '/de/nested' - }, - { - source: '/es/anidada', - destination: '/es/nested' - } - ]; - } -}); +module.exports = withNextIntl(); diff --git a/examples/example-next-13-advanced/src/app/[locale]/client/ClientContent.tsx b/examples/example-next-13-advanced/src/app/[locale]/client/ClientContent.tsx index 04fe17dc1..6c377fcbb 100644 --- a/examples/example-next-13-advanced/src/app/[locale]/client/ClientContent.tsx +++ b/examples/example-next-13-advanced/src/app/[locale]/client/ClientContent.tsx @@ -2,7 +2,7 @@ import {useNow} from 'next-intl'; import {usePathname} from 'next-intl/client'; -import Link from 'next-intl/link'; +import {Link} from '../../../navigation'; export default function ClientContent() { const now = useNow(); 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 index 4ddee65f9..2c361746b 100644 --- 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 @@ -1,6 +1,6 @@ 'use client'; -import {redirect} from 'next-intl/server'; +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 index bac9e11aa..f9ad554bf 100644 --- a/examples/example-next-13-advanced/src/app/[locale]/layout.tsx +++ b/examples/example-next-13-advanced/src/app/[locale]/layout.tsx @@ -8,6 +8,7 @@ import { getTranslator } from 'next-intl/server'; import {ReactNode} from 'react'; +import Navigation from '../../components/Navigation'; type Props = { children: ReactNode; @@ -42,7 +43,18 @@ export default function LocaleLayout({children, params}: Props) { return ( - {children} + +
+ + {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 index 0f7439f2a..b7ce8b5a2 100644 --- a/examples/example-next-13-advanced/src/app/[locale]/nested/page.tsx +++ b/examples/example-next-13-advanced/src/app/[locale]/nested/page.tsx @@ -1,5 +1,6 @@ import {useTranslations} from 'next-intl'; import PageLayout from '../../../components/PageLayout'; +import UnlocalizedPathname from './UnlocalizedPathname'; export default function Nested() { const t = useTranslations('Nested'); @@ -7,6 +8,7 @@ export default function 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]/page.tsx b/examples/example-next-13-advanced/src/app/[locale]/page.tsx index cdb414872..5d622e2d2 100644 --- a/examples/example-next-13-advanced/src/app/[locale]/page.tsx +++ b/examples/example-next-13-advanced/src/app/[locale]/page.tsx @@ -1,6 +1,5 @@ import Image from 'next/image'; import {useFormatter, useNow, useTimeZone, useTranslations} from 'next-intl'; -import Link from 'next-intl/link'; import ClientLink from '../../components/ClientLink'; import ClientRouterWithoutProvider from '../../components/ClientRouterWithoutProvider'; import CoreLibrary from '../../components/CoreLibrary'; @@ -8,6 +7,7 @@ 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; 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 index 5212ddf14..520e04565 100644 --- a/examples/example-next-13-advanced/src/app/[locale]/redirect/page.tsx +++ b/examples/example-next-13-advanced/src/app/[locale]/redirect/page.tsx @@ -1,4 +1,4 @@ -import {redirect} from 'next-intl/server'; +import {redirect} from '../../../navigation'; export default function Redirect() { redirect('/client'); diff --git a/examples/example-next-13-advanced/src/components/ClientLink.tsx b/examples/example-next-13-advanced/src/components/ClientLink.tsx index adcdf99c6..557ce7e88 100644 --- a/examples/example-next-13-advanced/src/components/ClientLink.tsx +++ b/examples/example-next-13-advanced/src/components/ClientLink.tsx @@ -1,7 +1,7 @@ 'use client'; -import Link from 'next-intl/link'; import {ComponentProps} from 'react'; +import {Link} from '../navigation'; type Props = ComponentProps; diff --git a/examples/example-next-13-advanced/src/components/ClientRouterWithoutProvider.tsx b/examples/example-next-13-advanced/src/components/ClientRouterWithoutProvider.tsx index 505458f09..24aef772f 100644 --- a/examples/example-next-13-advanced/src/components/ClientRouterWithoutProvider.tsx +++ b/examples/example-next-13-advanced/src/components/ClientRouterWithoutProvider.tsx @@ -1,6 +1,6 @@ 'use client'; -import {useRouter} from 'next-intl/client'; +import {useRouter} from '../navigation'; export default function ClientRouterWithoutProvider() { const router = useRouter(); diff --git a/examples/example-next-13-advanced/src/components/LocaleSwitcher.tsx b/examples/example-next-13-advanced/src/components/LocaleSwitcher.tsx index 8eb5039e6..4c241ba44 100644 --- a/examples/example-next-13-advanced/src/components/LocaleSwitcher.tsx +++ b/examples/example-next-13-advanced/src/components/LocaleSwitcher.tsx @@ -1,5 +1,5 @@ import {useLocale, useTranslations} from 'next-intl'; -import Link from 'next-intl/link'; +import {Link} from '../navigation'; export default function LocaleSwitcher() { const t = useTranslations('LocaleSwitcher'); diff --git a/examples/example-next-13-advanced/src/components/Navigation.tsx b/examples/example-next-13-advanced/src/components/Navigation.tsx index 8cd65eff9..3dbf67ef2 100644 --- a/examples/example-next-13-advanced/src/components/Navigation.tsx +++ b/examples/example-next-13-advanced/src/components/Navigation.tsx @@ -9,6 +9,9 @@ export default function Navigation() { {t('home')} {t('client')} {t('nested')} + + {t('newsArticle', {articleId: 3})} + ); } diff --git a/examples/example-next-13-advanced/src/components/NavigationLink.tsx b/examples/example-next-13-advanced/src/components/NavigationLink.tsx index a53c00d1a..838d96d10 100644 --- a/examples/example-next-13-advanced/src/components/NavigationLink.tsx +++ b/examples/example-next-13-advanced/src/components/NavigationLink.tsx @@ -1,16 +1,14 @@ 'use client'; -import {usePathname} from 'next-intl/client'; -import Link from 'next-intl/link'; -import {ReactNode} from 'react'; +import {useSelectedLayoutSegment} from 'next/navigation'; +import {ComponentProps} from 'react'; +import {Link} from '../navigation'; -type Props = { - children: ReactNode; - href: string; -}; +type Props = ComponentProps; -export default function NavigationLink({children, href}: Props) { - const pathname = usePathname(); +export default function NavigationLink({href, ...rest}: Props) { + const selectedLayoutSegment = useSelectedLayoutSegment(); + const pathname = selectedLayoutSegment ? `/${selectedLayoutSegment}` : '/'; const isActive = pathname === href; return ( @@ -18,8 +16,7 @@ export default function NavigationLink({children, href}: Props) { aria-current={isActive ? 'page' : undefined} href={href} style={{textDecoration: isActive ? 'underline' : 'none'}} - > - {children} - + {...rest} + /> ); } diff --git a/examples/example-next-13-advanced/src/components/PageLayout.tsx b/examples/example-next-13-advanced/src/components/PageLayout.tsx index 36f1b437e..cf5609c4d 100644 --- a/examples/example-next-13-advanced/src/components/PageLayout.tsx +++ b/examples/example-next-13-advanced/src/components/PageLayout.tsx @@ -1,5 +1,4 @@ import {ReactNode} from 'react'; -import Navigation from './Navigation'; type Props = { children?: ReactNode; @@ -8,18 +7,9 @@ type Props = { export default function PageLayout({children, title}: Props) { return ( -
- -
-

{title}

- {children} -
+
+

{title}

+ {children}
); } diff --git a/examples/example-next-13-advanced/src/config.tsx b/examples/example-next-13-advanced/src/config.tsx new file mode 100644 index 000000000..4466c4e9c --- /dev/null +++ b/examples/example-next-13-advanced/src/config.tsx @@ -0,0 +1,20 @@ +import {Pathnames} from 'next-intl/navigation'; + +export const locales = ['en', 'de', 'es'] as const; + +export const pathnames: 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]' + } +} as const; diff --git a/examples/example-next-13-advanced/src/middleware.ts b/examples/example-next-13-advanced/src/middleware.ts index 9a620120c..4e7a42eeb 100644 --- a/examples/example-next-13-advanced/src/middleware.ts +++ b/examples/example-next-13-advanced/src/middleware.ts @@ -1,8 +1,10 @@ import createMiddleware from 'next-intl/middleware'; +import {locales, pathnames} from './config'; export default createMiddleware({ - locales: ['en', 'de', 'es'], - defaultLocale: 'en' + defaultLocale: 'en', + pathnames, + locales }); export const config = { 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..d98738ffe --- /dev/null +++ b/examples/example-next-13-advanced/src/navigation.tsx @@ -0,0 +1,8 @@ +import {createLocalizedPathnamesNavigation} from 'next-intl/navigation'; +import {locales, pathnames} from './config'; + +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 index 6b4b78764..e00263726 100644 --- a/examples/example-next-13-advanced/tests/main.spec.ts +++ b/examples/example-next-13-advanced/tests/main.spec.ts @@ -91,7 +91,7 @@ it('redirects unprefixed paths for non-default locales', async ({browser}) => { const page = await context.newPage(); await page.goto('/nested'); - await expect(page).toHaveURL('/de/nested'); + await expect(page).toHaveURL('/de/verschachtelt'); page.getByRole('heading', {name: 'Verschachtelt'}); }); @@ -349,6 +349,14 @@ it('can use `usePathname`', async ({page}) => { 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 }) => { @@ -397,7 +405,7 @@ it('prefixes routes as necessary with the router', async ({page}) => { await page.goto('/de'); page.getByTestId('ClientRouterWithoutProvider-link').click(); - await expect(page).toHaveURL('/de/nested'); + await expect(page).toHaveURL('/de/verschachtelt'); }); it('can set `now` and `timeZone` at runtime', async ({page}) => { @@ -438,7 +446,8 @@ it('keeps search params for redirects', async ({browser}) => { ); }); -it('sets alternate links', async ({request}) => { +// TODO +it.skip('sets alternate links', async ({request}) => { async function getLinks(pathname: string) { return ( (await request.get(pathname)) @@ -470,16 +479,28 @@ it('sets alternate links', async ({request}) => { } }); -it('can use rewrites to localize pathnames', async ({page, request}) => { +it('can use rewrites to localize pathnames', async ({page}) => { await page.goto('/de/verschachtelt'); page.getByRole('heading', {name: 'Verschachtelt'}); - // Also available + // 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'}); - - const response = await request.get('/en/verschachtelt'); - expect(response.status()).toBe(404); + 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 ({request}) => { diff --git a/examples/example-next-13/src/components/NavigationLink.tsx b/examples/example-next-13/src/components/NavigationLink.tsx index ed9f46d29..9e8b4ecbe 100644 --- a/examples/example-next-13/src/components/NavigationLink.tsx +++ b/examples/example-next-13/src/components/NavigationLink.tsx @@ -14,7 +14,7 @@ export default function NavigationLink({href, ...rest}: Props) { return ( = { '/': '/', '/about': { en: '/about', diff --git a/examples/example-next-13/src/navigation.tsx b/examples/example-next-13/src/navigation.tsx index f29436b85..ec3ce5aaf 100644 --- a/examples/example-next-13/src/navigation.tsx +++ b/examples/example-next-13/src/navigation.tsx @@ -1,10 +1,11 @@ -import {createNamedNavigation} from 'next-intl/navigation'; +import {createLocalizedPathnamesNavigation} from 'next-intl/navigation'; import {locales, pathnames} from './i18n'; -export const {Link, redirect, usePathname, useRouter} = createNamedNavigation({ - locales, - pathnames -}); +export const {Link, redirect, usePathname, useRouter} = + createLocalizedPathnamesNavigation({ + locales, + pathnames + }); // export const {Link, redirect, useRouter} = createDefaultNavigation({ // locales, diff --git a/packages/next-intl/package.json b/packages/next-intl/package.json index f4c4d0c04..df0197bb6 100644 --- a/packages/next-intl/package.json +++ b/packages/next-intl/package.json @@ -81,7 +81,6 @@ "@formatjs/intl-localematcher": "^0.2.32", "negotiator": "^0.6.3", "server-only": "0.0.1", - "path-to-regexp": "^6.2.1", "use-intl": "^2.19.0" }, "peerDependencies": { diff --git a/packages/next-intl/src/middleware/LocalizedPathnames.tsx b/packages/next-intl/src/middleware/LocalizedPathnames.tsx index 82ddfaddf..a8d4cb2c8 100644 --- a/packages/next-intl/src/middleware/LocalizedPathnames.tsx +++ b/packages/next-intl/src/middleware/LocalizedPathnames.tsx @@ -22,13 +22,19 @@ export function getLocalizedRedirectPathname( configWithDefaults.locales ); - for (const [, routePath] of Object.entries(configWithDefaults.pathnames)) { - if (typeof routePath === 'string') { + // TODO + // what if there's no path locale? assume default locale? + // we can't use the defaultLocale btw., but should consult the domain config + + for (const [, localizedPathnames] of Object.entries( + configWithDefaults.pathnames + )) { + if (typeof localizedPathnames === 'string') { // No redirect is necessary if all locales use the same pathname continue; } - for (const [locale, localePathname] of Object.entries(routePath)) { + for (const [locale, localePathname] of Object.entries(localizedPathnames)) { if (resolvedLocale === locale) { continue; } @@ -45,7 +51,10 @@ export function getLocalizedRedirectPathname( if (resolvedLocale !== configWithDefaults.defaultLocale || pathLocale) { targetPathname = `/${resolvedLocale}`; } - targetPathname += formatPathname(routePath[resolvedLocale], params); + targetPathname += formatPathname( + localizedPathnames[resolvedLocale], + params + ); return getPathWithSearch(targetPathname, request.nextUrl.search); } @@ -71,30 +80,39 @@ export function getLocalizedRewritePathname( configWithDefaults.locales ); + // TODO + // this assumption is wrong + // maybe we should consult the domain config here to figure out the default locale and then use it to map a localized pathname to an internal one + // generally test the case when the internal path is different than the default locale + // const locale = pathLocale ?? configWithDefaults.defaultLocale; if ( // When using unprefixed routing, we assume that the // pathname uses routes from the default locale - !pathLocale || - // Internal routes are set up based on the default locale - pathLocale === configWithDefaults.defaultLocale + !pathLocale ) { return; } - for (const [, routePath] of Object.entries(configWithDefaults.pathnames)) { - if (typeof routePath === 'string') { + for (const [internalPathname, localizedPathnames] of Object.entries( + configWithDefaults.pathnames + )) { + if (typeof localizedPathnames === 'string') { // No rewrite is necessary if all locales use the same pathname continue; } - const defaultLocalePathname = routePath[configWithDefaults.defaultLocale]; - const pathLocalePathname = `/${pathLocale}${routePath[pathLocale]}`; + if (internalPathname === localizedPathnames[pathLocale]) { + // No rewrite is necessary if the localized pathname matches the internal one + continue; + } + + const pathLocalePathname = `/${pathLocale}${localizedPathnames[pathLocale]}`; const matches = matchesPathname(pathLocalePathname, pathname); if (matches) { const params = getRouteParams(pathLocalePathname, pathname); return getPathWithSearch( - `/${pathLocale}` + formatPathname(defaultLocalePathname, params), + `/${pathLocale}` + formatPathname(internalPathname, params), request.nextUrl.search ); } diff --git a/packages/next-intl/src/middleware/utils.tsx b/packages/next-intl/src/middleware/utils.tsx index 41b913db8..c62996cff 100644 --- a/packages/next-intl/src/middleware/utils.tsx +++ b/packages/next-intl/src/middleware/utils.tsx @@ -1,4 +1,3 @@ -import {pathToRegexp, match, compile} from 'path-to-regexp'; import {AllLocales} from '../shared/types'; import {DomainConfig} from './NextIntlMiddlewareConfig'; @@ -21,25 +20,41 @@ export function getBasePath(pathname: string, pathLocale: string) { return pathname.replace(`/${pathLocale}`, '') || '/'; } -export function matchesPathname(template: string, pathname: string) { - const regex = pathToRegexp(template); - const matches = regex.exec(pathname); - return matches != null; +function templateToRegex(template: string): RegExp { + const regexPattern = template.replace(/\[([^\]]+)\]/g, '([^/]+)'); + return new RegExp(`^${regexPattern}$`); } -export function getRouteParams(template: string, pathname: string) { - const fn = match( - template - // { decode: decodeURIComponent } - ); +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); +} - const result = fn(pathname); - return result ? result.params : undefined; +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) { - const toPath = compile(template); - return toPath(params); + if (!params) return template; + + let result = template; + Object.entries(params).forEach(([key, value]) => { + result = result.replace(`[${key}]`, value); + }); + return result; } export function getPathWithSearch( diff --git a/packages/next-intl/src/navigation/createNamedNavigation.tsx b/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx similarity index 74% rename from packages/next-intl/src/navigation/createNamedNavigation.tsx rename to packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx index 6999be595..e8279b758 100644 --- a/packages/next-intl/src/navigation/createNamedNavigation.tsx +++ b/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx @@ -6,16 +6,17 @@ import { import useClientLocale from '../client/useClientLocale'; import BaseLink from '../link'; import baseRedirect from '../server/react-client/redirect'; -import {AllLocales, ParametersExceptFirst, Pathnames} from '../shared/types'; -import {Params, compileNamedRoute, getNamedRoute} from './utils'; +import { + AllLocales, + HrefOrUrlObject, + ParametersExceptFirst, + Pathnames +} from '../shared/types'; +import {Params, compileLocalizedPathname, getNamedRoute} from './utils'; -export default function createNamedNavigation({ - locales, - pathnames -}: { - locales: Locales; - pathnames: Pathnames; -}) { +export default function createLocalizedPathnamesNavigation< + Locales extends AllLocales +>({locales, pathnames}: {locales: Locales; pathnames: Pathnames}) { function useLocale() { return useClientLocale() as (typeof locales)[number]; } @@ -28,7 +29,7 @@ export default function createNamedNavigation({ params, ...rest }: Omit, 'href' | 'name'> & { - href: keyof Pathnames; + href: HrefOrUrlObject; params?: Params; locale?: Locales[number]; }, @@ -40,12 +41,14 @@ export default function createNamedNavigation({ return ( ({ locale: finalLocale, + // @ts-expect-error -- No idea href, params, pathnames })} + locale={locale} {...rest} /> ); @@ -53,7 +56,7 @@ export default function createNamedNavigation({ ); Link.displayName = 'Link'; - type NameOrNameWithParams = + type HrefOrHrefWithParams = | keyof Pathnames | { href: keyof Pathnames; @@ -61,7 +64,7 @@ export default function createNamedNavigation({ }; function normalizeNameOrNameWithParams( - nameOrNameWithParams: NameOrNameWithParams + nameOrNameWithParams: HrefOrHrefWithParams ) { return typeof nameOrNameWithParams === 'string' ? {href: nameOrNameWithParams, params: undefined} @@ -69,12 +72,12 @@ export default function createNamedNavigation({ } function redirect( - nameOrNameWithParams: NameOrNameWithParams, + nameOrNameWithParams: HrefOrHrefWithParams, ...args: ParametersExceptFirst ) { // eslint-disable-next-line react-hooks/rules-of-hooks -- Reading from context conditionally is fine const locale = useLocale(); - const href = compileNamedRoute({ + const href = compileLocalizedPathname({ ...normalizeNameOrNameWithParams(nameOrNameWithParams), locale, pathnames @@ -89,10 +92,10 @@ export default function createNamedNavigation({ return { ...baseRouter, push( - nameOrNameWithParams: NameOrNameWithParams, + nameOrNameWithParams: HrefOrHrefWithParams, ...args: ParametersExceptFirst ) { - const href = compileNamedRoute({ + const href = compileLocalizedPathname({ ...normalizeNameOrNameWithParams(nameOrNameWithParams), locale, pathnames @@ -101,10 +104,10 @@ export default function createNamedNavigation({ }, replace( - nameOrNameWithParams: NameOrNameWithParams, + nameOrNameWithParams: HrefOrHrefWithParams, ...args: ParametersExceptFirst ) { - const href = compileNamedRoute({ + const href = compileLocalizedPathname({ ...normalizeNameOrNameWithParams(nameOrNameWithParams), locale, pathnames @@ -113,10 +116,10 @@ export default function createNamedNavigation({ }, prefetch( - nameOrNameWithParams: NameOrNameWithParams, + nameOrNameWithParams: HrefOrHrefWithParams, ...args: ParametersExceptFirst ) { - const href = compileNamedRoute({ + const href = compileLocalizedPathname({ ...normalizeNameOrNameWithParams(nameOrNameWithParams), locale, pathnames diff --git a/packages/next-intl/src/navigation/index.tsx b/packages/next-intl/src/navigation/index.tsx index cd561943e..166dc1263 100644 --- a/packages/next-intl/src/navigation/index.tsx +++ b/packages/next-intl/src/navigation/index.tsx @@ -1 +1,2 @@ -export {default as createNamedNavigation} from './createNamedNavigation'; +export {default as createLocalizedPathnamesNavigation} from './createLocalizedPathnamesNavigation'; +export {Pathnames} from '../shared/types'; diff --git a/packages/next-intl/src/navigation/react-server/createNamedNavigation.tsx b/packages/next-intl/src/navigation/react-server/createLocalizedPathnamesNavigation.tsx similarity index 71% rename from packages/next-intl/src/navigation/react-server/createNamedNavigation.tsx rename to packages/next-intl/src/navigation/react-server/createLocalizedPathnamesNavigation.tsx index e3f0fd427..307665877 100644 --- a/packages/next-intl/src/navigation/react-server/createNamedNavigation.tsx +++ b/packages/next-intl/src/navigation/react-server/createLocalizedPathnamesNavigation.tsx @@ -1,17 +1,13 @@ import React, {ComponentProps} from 'react'; import BaseLink from '../../link/react-server'; -import {redirect as baseRedirect} from '../../server'; import getLocaleFromHeader from '../../server/getLocaleFromHeader'; +import {redirect as baseRedirect} from '../../server.react-server'; import {AllLocales, ParametersExceptFirst, Pathnames} from '../../shared/types'; -import {Params, compileNamedRoute} from '../utils'; +import {Params, compileLocalizedPathname} from '../utils'; -export default function createNamedNavigation({ - locales, - pathnames -}: { - locales: Locales; - pathnames: Pathnames; -}) { +export default function createLocalizedPathnamesNavigation< + Locales extends AllLocales +>({locales, pathnames}: {locales: Locales; pathnames: Pathnames}) { function Link({ href, locale, @@ -27,12 +23,13 @@ export default function createNamedNavigation({ return ( ); @@ -53,7 +50,7 @@ export default function createNamedNavigation({ : nameOrNameWithParams; const locale = getLocaleFromHeader(); - const href = compileNamedRoute({ + const href = compileLocalizedPathname({ locale, href: name, params, @@ -64,9 +61,11 @@ export default function createNamedNavigation({ } function notSupported(message: string) { - 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 () => { + 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 { diff --git a/packages/next-intl/src/navigation/react-server/index.tsx b/packages/next-intl/src/navigation/react-server/index.tsx index cd561943e..6b30ca63b 100644 --- a/packages/next-intl/src/navigation/react-server/index.tsx +++ b/packages/next-intl/src/navigation/react-server/index.tsx @@ -1 +1 @@ -export {default as createNamedNavigation} from './createNamedNavigation'; +export {default as createLocalizedPathnamesNavigation} from './createLocalizedPathnamesNavigation'; diff --git a/packages/next-intl/src/navigation/utils.tsx b/packages/next-intl/src/navigation/utils.tsx index 43f803469..62771533f 100644 --- a/packages/next-intl/src/navigation/utils.tsx +++ b/packages/next-intl/src/navigation/utils.tsx @@ -1,40 +1,73 @@ -import {AllLocales, Pathnames} from '../shared/types'; +import type {UrlObject} from 'url'; +import {AllLocales, Pathnames, StrictUrlObject} from '../shared/types'; // TODO: Can we type this? -export type Params = Record; +export type Params = Record; -export function compileNamedRoute({ - href: name, +export function compileLocalizedPathname(opts: { + locale: Locales[number]; + // eslint-disable-next-line no-use-before-define -- False positive + href: keyof typeof opts.pathnames; + params?: Params; + pathnames: Pathnames; +}): string; +export function compileLocalizedPathname(opts: { + locale: Locales[number]; + // eslint-disable-next-line no-use-before-define -- False positive + href: StrictUrlObject; + params?: Params; + pathnames: Pathnames; +}): UrlObject; +export function compileLocalizedPathname({ + href, locale, params, pathnames }: { locale: Locales[number]; - href: keyof Pathnames; + href: keyof typeof pathnames | StrictUrlObject; params?: Params; pathnames: Pathnames; }) { - const namedPath = pathnames[name]; - - if (!namedPath) { - throw new Error( - `No named route found for "${name}". Available routes: ${Object.keys( - pathnames - ).join(', ')}` - ); + function getNamedPath(value: keyof typeof pathnames) { + const namedPath = pathnames[value]; + if (!namedPath) { + throw new Error( + `No named route found for "${value}". Available routes: ${Object.keys( + pathnames + ).join(', ')}` + ); + } + return namedPath; } - const href = typeof namedPath === 'string' ? namedPath : namedPath[locale]; - if (params) { - // Object.keys(params).forEach((param) => { - // href = href.replace( - // new RegExp(':' + param, 'g'), - // (params as any)[param] - // ); - // }); + function compilePath( + namedPath: Pathnames[keyof Pathnames] + ) { + let compiled = + typeof namedPath === 'string' ? namedPath : namedPath[locale]; + + if (params) { + Object.entries(params).forEach(([key, value]) => { + compiled = compiled.replace(`[${key}]`, String(value)); + }); + } + // Error handling if there are unresolved params + + return compiled; } - return href; + if (typeof href === 'string') { + const namedPath = getNamedPath(href); + const compiled = compilePath(namedPath); + return compiled; + } else { + const {pathname, ...rest} = href; + const namedPath = getNamedPath(pathname); + const compiled = compilePath(namedPath); + const result: UrlObject = {...rest, pathname: compiled}; + return result; + } } export function getNamedRoute({ diff --git a/packages/next-intl/src/shared/types.tsx b/packages/next-intl/src/shared/types.tsx index 672173427..46c7b0671 100644 --- a/packages/next-intl/src/shared/types.tsx +++ b/packages/next-intl/src/shared/types.tsx @@ -1,3 +1,5 @@ +import type {UrlObject} from 'url'; + export type Locale = string; export type AllLocales = ReadonlyArray; @@ -6,6 +8,17 @@ export type Pathnames = Record< {[Key in Locales[number]]: string} | string >; +export type StrictUrlObject = Omit< + UrlObject, + 'pathname' +> & { + pathname: Pathname; +}; + +export type HrefOrUrlObject = + | Pathname + | StrictUrlObject; + export type ParametersExceptFirst = Fn extends ( arg0: any, ...rest: infer R diff --git a/packages/next-intl/test/link/Link.test.tsx b/packages/next-intl/test/link/Link.test.tsx index d6330fd36..af0ba14e9 100644 --- a/packages/next-intl/test/link/Link.test.tsx +++ b/packages/next-intl/test/link/Link.test.tsx @@ -21,9 +21,9 @@ describe('unprefixed routing', () => { }); 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' ); }); diff --git a/packages/next-intl/test/middleware/middleware.test.tsx b/packages/next-intl/test/middleware/middleware.test.tsx index 423c240f3..3e1372dc2 100644 --- a/packages/next-intl/test/middleware/middleware.test.tsx +++ b/packages/next-intl/test/middleware/middleware.test.tsx @@ -227,22 +227,22 @@ describe('prefix-based routing', () => { defaultLocale: 'en', locales: ['en', 'de'], pathnames: { - home: '/', - about: { + '/': '/', + '/about': { en: '/about', de: '/ueber' }, - users: { + '/users': { en: '/users', de: '/benutzer' }, - 'users-detail': { - en: '/users/:userId', - de: '/benutzer/:userId' + '/users/[userId]': { + en: '/users/[userId]', + de: '/benutzer/[userId]' }, - 'news-detail': { - en: '/news/:articleSlug-:articleId', - de: '/neuigkeiten/:articleSlug-:articleId' + '/news/[articleSlug]-[articleId]': { + en: '/news/[articleSlug]-[articleId]', + de: '/neuigkeiten/[articleSlug]-[articleId]' } } }); @@ -447,22 +447,22 @@ describe('prefix-based routing', () => { locales: ['en', 'de'], localePrefix: 'always', pathnames: { - home: '/', - about: { + '/': '/', + '/about': { en: '/about', de: '/ueber' }, - users: { + '/users': { en: '/users', de: '/benutzer' }, - 'users-detail': { - en: '/users/:userId', - de: '/benutzer/:userId' + '/users/[userId]': { + en: '/users/[userId]', + de: '/benutzer/[userId]' }, - 'news-detail': { - en: '/news/:articleSlug-:articleId', - de: '/neuigkeiten/:articleSlug-:articleId' + '/news/[articleSlug]-[articleId]': { + en: '/news/[articleSlug]-[articleId]', + de: '/neuigkeiten/[articleSlug]-[articleId]' } } }); 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..4cd9aae7e --- /dev/null +++ b/packages/next-intl/test/middleware/utils.test.tsx @@ -0,0 +1,149 @@ +import {describe, expect, it} from 'vitest'; +import { + formatPathname, + getRouteParams, + matchesPathname +} from '../../src/middleware/utils'; + +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); + }); + + 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); + }); +}); + +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.only('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' + }); + }); +}); + +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('handles special characters in parameter values', () => { + expect(formatPathname('/users/[userId]', {userId: '23 jane'})).toBe( + '/users/23%20jane' + ); + expect(formatPathname('/users/[userId]', {userId: '23/42'})).toBe( + '/users/23%2F42' + ); + }); +}); 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..3d5d9cd92 --- /dev/null +++ b/packages/next-intl/test/navigation/createLocalizedPathnamesNavigation.test.tsx @@ -0,0 +1,69 @@ +import {render, screen} from '@testing-library/react'; +import {usePathname, useParams} from 'next/navigation'; +import React from 'react'; +import {it, describe, vi, beforeEach, expect} from 'vitest'; +import {createLocalizedPathnamesNavigation} from '../../src/navigation'; + +vi.mock('next/navigation'); + +const {Link} = createLocalizedPathnamesNavigation({ + locales: ['en', 'de'], + pathnames: { + '/': '/', + '/about': { + en: '/about', + de: '/ueber-uns' + }, + '/news/[articleSlug]-[articleId]': { + en: '/news/[articleSlug]-[articleId]', + de: '/neuigkeiten/[articleSlug]-[articleId]' + } + } +}); + +beforeEach(() => { + 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/ueber-uns'); + }); + + it('handles params', () => { + render( + + About + + ); + expect(screen.getByRole('link', {name: 'About'}).getAttribute('href')).toBe( + '/de/neuigkeiten/launch-party-3' + ); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4acd07f18..eebce662b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -504,9 +504,6 @@ importers: negotiator: specifier: ^0.6.3 version: 0.6.3 - path-to-regexp: - specifier: ^6.2.1 - version: 6.2.1 server-only: specifier: 0.0.1 version: 0.0.1 @@ -669,7 +666,7 @@ packages: '@babel/traverse': 7.21.5 '@babel/types': 7.21.5 convert-source-map: 1.9.0 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.0 @@ -691,7 +688,7 @@ packages: '@babel/traverse': 7.22.5 '@babel/types': 7.22.5 convert-source-map: 1.9.0 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.0 @@ -713,7 +710,7 @@ packages: '@babel/traverse': 7.22.8 '@babel/types': 7.22.5 convert-source-map: 1.9.0 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -932,7 +929,7 @@ packages: '@babel/core': 7.22.5 '@babel/helper-compilation-targets': 7.22.5(@babel/core@7.22.5) '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 lodash.debounce: 4.0.8 resolve: 1.22.2 semver: 6.3.0 @@ -947,7 +944,7 @@ packages: '@babel/core': 7.22.5 '@babel/helper-compilation-targets': 7.22.5(@babel/core@7.22.5) '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 lodash.debounce: 4.0.8 resolve: 1.22.2 semver: 6.3.0 @@ -3753,7 +3750,7 @@ packages: '@babel/helper-split-export-declaration': 7.18.6 '@babel/parser': 7.21.8 '@babel/types': 7.21.5 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -3770,7 +3767,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.5 '@babel/parser': 7.22.5 '@babel/types': 7.22.5 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -3787,7 +3784,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.22.7 '@babel/types': 7.22.5 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -4269,7 +4266,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 espree: 9.5.1 globals: 13.20.0 ignore: 5.2.4 @@ -4722,7 +4719,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -6657,7 +6654,7 @@ packages: resolution: {integrity: sha512-dNZafjM93Y+F+sfwTO5gTpsGXlnc/0Q+c2+62ViqP3gkMWvHEMSKkaEHgVJLcLg3i/g19GSIPziiKpgyne07Bw==} engines: {node: '>=8'} dependencies: - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color dev: true @@ -7189,11 +7186,11 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.5.1 - '@typescript-eslint/parser': 5.62.0(eslint@8.39.0)(typescript@5.0.4) + '@typescript-eslint/parser': 5.62.0(eslint@8.39.0)(typescript@4.9.5) '@typescript-eslint/scope-manager': 5.59.2 '@typescript-eslint/type-utils': 5.59.2(eslint@8.39.0)(typescript@4.9.5) '@typescript-eslint/utils': 5.59.2(eslint@8.39.0)(typescript@4.9.5) - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 eslint: 8.39.0 grapheme-splitter: 1.0.4 ignore: 5.2.4 @@ -7237,8 +7234,8 @@ packages: dependencies: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.0.4) - debug: 4.3.4(supports-color@6.1.0) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) + debug: 4.3.4 eslint: 8.39.0 typescript: 4.9.5 transitivePeerDependencies: @@ -7258,7 +7255,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.0.4) - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 eslint: 8.39.0 typescript: 5.0.4 transitivePeerDependencies: @@ -7293,7 +7290,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 5.59.2(typescript@4.9.5) '@typescript-eslint/utils': 5.59.2(eslint@8.39.0)(typescript@4.9.5) - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 eslint: 8.39.0 tsutils: 3.21.0(typescript@4.9.5) typescript: 4.9.5 @@ -7322,7 +7319,7 @@ packages: dependencies: '@typescript-eslint/types': 5.59.2 '@typescript-eslint/visitor-keys': 5.59.2 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.3 @@ -7343,7 +7340,7 @@ packages: dependencies: '@typescript-eslint/types': 5.59.2 '@typescript-eslint/visitor-keys': 5.59.2 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.3 @@ -7353,6 +7350,27 @@ packages: - 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 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.0 + tsutils: 3.21.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.0.4): resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7364,7 +7382,7 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.0 @@ -7943,7 +7961,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -10918,6 +10936,17 @@ packages: ms: 2.0.0 supports-color: 6.1.0 + /debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + /debug@3.2.7(supports-color@6.1.0): resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -10929,6 +10958,17 @@ packages: ms: 2.1.3 supports-color: 6.1.0 + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + /debug@4.3.4(supports-color@6.1.0): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -11464,7 +11504,7 @@ packages: '@rollup/plugin-replace': 3.1.0(rollup@2.79.1) '@types/jest': 27.5.2 '@typescript-eslint/eslint-plugin': 5.59.2(@typescript-eslint/parser@5.62.0)(eslint@8.39.0)(typescript@4.9.5) - '@typescript-eslint/parser': 5.62.0(eslint@8.39.0)(typescript@4.9.5) + '@typescript-eslint/parser': 5.62.0(eslint@8.39.0)(typescript@5.0.4) ansi-escapes: 4.3.2 asyncro: 3.0.0 babel-plugin-annotate-pure-calls: 0.4.0(@babel/core@7.22.5) @@ -11913,7 +11953,7 @@ packages: dependencies: '@rushstack/eslint-patch': 1.2.0 '@typescript-eslint/eslint-plugin': 5.59.2(@typescript-eslint/parser@5.62.0)(eslint@8.39.0)(typescript@4.9.5) - '@typescript-eslint/parser': 5.62.0(eslint@8.39.0)(typescript@5.0.4) + '@typescript-eslint/parser': 5.62.0(eslint@8.39.0)(typescript@4.9.5) confusing-browser-globals: 1.0.11 eslint: 8.39.0 eslint-plugin-css-modules: 2.11.0(eslint@8.39.0) @@ -12002,7 +12042,7 @@ packages: /eslint-import-resolver-node@0.3.7: resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} dependencies: - debug: 3.2.7(supports-color@6.1.0) + debug: 3.2.7 is-core-module: 2.12.0 resolve: 1.22.2 transitivePeerDependencies: @@ -12084,8 +12124,8 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.39.0)(typescript@5.0.4) - debug: 3.2.7(supports-color@6.1.0) + '@typescript-eslint/parser': 5.62.0(eslint@8.39.0)(typescript@4.9.5) + debug: 3.2.7 eslint: 8.39.0 eslint-import-resolver-node: 0.3.7 transitivePeerDependencies: @@ -12143,11 +12183,11 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.39.0)(typescript@5.0.4) + '@typescript-eslint/parser': 5.62.0(eslint@8.39.0)(typescript@4.9.5) array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 - debug: 3.2.7(supports-color@6.1.0) + debug: 3.2.7 doctrine: 2.1.0 eslint: 8.39.0 eslint-import-resolver-node: 0.3.7 @@ -12395,7 +12435,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.0 @@ -12940,7 +12980,7 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true dependencies: - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -14463,7 +14503,7 @@ packages: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color dev: true @@ -14524,7 +14564,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -15387,7 +15427,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 istanbul-lib-coverage: 3.2.0 source-map: 0.6.1 transitivePeerDependencies: @@ -20150,10 +20190,6 @@ packages: /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} - /path-to-regexp@6.2.1: - resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} - dev: false - /path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} engines: {node: '>=4'} @@ -21234,7 +21270,7 @@ packages: engines: {node: '>=10.18.1'} dependencies: cross-fetch: 3.1.5 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 devtools-protocol: 0.0.981744 extract-zip: 2.0.1 https-proxy-agent: 5.0.1 @@ -24908,7 +24944,7 @@ packages: hasBin: true dependencies: cac: 6.7.14 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 mlly: 1.2.0 pathe: 1.1.0 picocolors: 1.0.0 @@ -25001,7 +25037,7 @@ packages: cac: 6.7.14 chai: 4.3.7 concordance: 5.0.4 - debug: 4.3.4(supports-color@6.1.0) + debug: 4.3.4 local-pkg: 0.4.3 magic-string: 0.30.0 pathe: 1.1.0 From 90d1452fadebb00381702c7fab4d18710cef69d9 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 28 Jul 2023 17:40:03 +0200 Subject: [PATCH 09/37] Docs --- docs/pages/docs/routing/middleware.mdx | 58 +++-- docs/pages/docs/routing/navigation.mdx | 244 ++++++++++++++++-- .../src/components/LocaleSwitcherSelect.tsx | 4 +- .../createLocalizedPathnamesNavigation.tsx | 6 +- .../createLocalizedPathnamesNavigation.tsx | 4 +- packages/next-intl/src/navigation/utils.tsx | 16 +- 6 files changed, 280 insertions(+), 52 deletions(-) diff --git a/docs/pages/docs/routing/middleware.mdx b/docs/pages/docs/routing/middleware.mdx index 8e2710705..3b75852c9 100644 --- a/docs/pages/docs/routing/middleware.mdx +++ b/docs/pages/docs/routing/middleware.mdx @@ -31,7 +31,7 @@ There are two strategies for detecting the locale: Once a locale is detected, it will be saved in a cookie. -### Prefix-based routing (default) [#prefix-based-routing] +### Strategy 1: Prefix-based routing (default) [#prefix-based-routing] Since your pages are nested within a `[locale]` folder, all routes are prefixed with one of your supported locales (e.g. `/de/about`). To keep the URL short, requests for the default locale are rewritten internally to work without a locale prefix. @@ -60,7 +60,7 @@ To change the locale, users can visit a prefixed route. This will take precedenc 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 `/`. -### Domain-based routing +### 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. @@ -203,27 +203,53 @@ export default createMiddleware({ ## Localizing pathnames -If you want to localize the pathnames of your app, you can accomplish this by using appropriate [rewrites](https://nextjs.org/docs/api-reference/next.config.js/rewrites). +Many apps choose to localize pathnames, especially when search engine optimization is relevant, e.g.: -```js filename="next.config.js" {7-8} -const withNextIntl = require('next-intl/plugin')(); +- `/en/about` +- `/de/ueber-uns` -module.exports = withNextIntl({ - rewrites() { - return [ - { - source: '/de/über', - destination: '/de/about' - } - ]; +Since you want to define these routes only once internally, you can use the `next-intl` middleware to [rewrite](https://nextjs.org/docs/api-reference/next.config.js/rewrites) such incoming requests to shared paths internally. + +```js filename="middleware.ts" +import createMiddleware from 'next-intl/middleware'; + +export default createMiddleware({ + defaultLocale: 'en', + locales: ['en', 'de'], + + // The `pathnames` object holds pairs of internal and + // external paths. Based on the locale, the external + // paths are rewritten to the shared, internal ones. + pathnames: { + // If all locales use the same paths, a single + // external path can be used for all locales. + '/': '/', + '/blog': '/blog', + + // If locales use different paths, you can + // specify each external path per locale. + '/about': { + en: '/about', + de: '/ueber-uns' + }, + + // Dynamic params are supported via square brackets + '/news/[articleSlug]-[articleId]': { + en: '/news/[articleSlug]-[articleId]', + de: '/neuigkeiten/[articleSlug]-[articleId]' + } } }); ``` -Since `next-intl` isn't aware of the rewrites you've configured, you likely want to make some adjustments: +Note that the middleware will adapt the returned [alternate links](http://localhost:3000/docs/routing/middleware#disable-alternate-links) depending on this configuration. -1. Translate the pathnames you're passing to [navigation APIs](/docs/routing/navigation) like `Link` based on the `locale`. See the [named routes example](https://github.com/amannn/next-intl/blob/feat/next-13-rsc/examples/example-next-13-named-routes/) that uses the proposed APIs from [the Server Components beta](https://next-intl-docs.vercel.app/docs/getting-started/app-router-server-components). -2. Turn off [the `alternateLinks` option](/docs/routing/middleware#disable-alternate-links) and provide [search engine hints about localized versions of your content](https://developers.google.com/search/docs/specialty/international/localized-versions) by yourself. + + If you have pathname localization set up in your middleware, you likely want + to use the [localized navigation + APIs](/docs/routing/navigation#strategy-2-localized-pathnames) in your + components. + ## Composing other middlewares diff --git a/docs/pages/docs/routing/navigation.mdx b/docs/pages/docs/routing/navigation.mdx index 044853ba7..1212ac9a0 100644 --- a/docs/pages/docs/routing/navigation.mdx +++ b/docs/pages/docs/routing/navigation.mdx @@ -1,31 +1,140 @@ import Callout from 'components/Callout'; +import {Tab, Tabs} from 'nextra-theme-docs'; # Next.js 13 internationalized navigation `next-intl` provides drop-in replacements for common Next.js navigation APIs that automatically handle the user locale behind the scenes. -## `Link` +## Strategies + +There are two strategies that you can use based on your needs: + +1. [Shared pathnames](#shared-pathnames) +2. [Localized pathnames](#localized-pathnames) + +Each strategy will provide you with corresponding [navigation APIs](#apis) that you can use in your components. Typically, you'll provide these APIs in a central module that you can easily reference (e.g. `./navigation.ts`). + +### Strategy 1: Shared pathnames [#shared-pathnames] + +The simplest case is when your app uses the same pathnames, regardless of the locale, e.g.: + +- `/en/about` +- `/en-GB/about` + +In this example, the path segment `about` remains the same, regardless of the user's locale. This is the simplest case, because the routes you define in Next.js will map directly to the URLs that a user can request. + +To create [navigation APIs](#apis) for this use case, use the `createSharedPathnamesNavigation` function: + +```tsx filename="./navigation.tsx" +import {createSharedPathnamesNavigation} from 'next-intl/navigation'; + +export const {Link, redirect, usePathname, useRouter} = + createSharedPathnamesNavigation({ + locales: ['en', 'en-GB'], + pathnames: { + '/': '/', + '/about': { + en: '/about', + de: '/ueber-uns' + } + } + }); +``` + +The `pathnames` argument is identical to the configuration that you pass to the middleware for [localizing pathnames](/docs/routing/middleware#localizing-pathnames). Because of this, you might want to share it along with the `locales` from a shared file like `i18n.ts`: + +```tsx filename="i18n.ts" +import {Pathnames} from 'next-intl/navigation'; + +export const locales = ['en', 'de'] as const; + +export const pathnames: Pathnames = { + '/': '/', + '/about': { + en: '/about', + de: '/ueber-uns' + } +} as const; +``` + +Have a look at the [App Router example](/examples/app-router) to explore a working implementation of localized pathnames. + +### Strategy 2: Localized pathnames [#localized-pathnames] + +If you've configured the middleware to [use localized pathnames](/docs/routing/middleware#localizing-pathnames), you can use the `createLocalizedPathnamesNavigation` function to create corresponding navigation APIs: + +'re using the middleware configuration localized pathnames in the + +Many apps choose to localize pathnames, especially when search engine optimization is relevant: + +```tsx filename="./navigation.tsx" +import {createLocalizedPathnamesNavigation} from 'next-intl/navigation'; + +export const {Link, redirect, usePathname, useRouter} = + createLocalizedPathnamesNavigation({ + locales: ['en', 'en-GB'] + }); +``` + +--- + +## APIs + +### `Link` 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 // You can override the `locale` to switch to another language Switch to German + +// Dynamic params need to be interpolated into the pathname +Susan +``` + + + +When using [localized pathnames](#shared-pathnames), the `href` prop corresponds to an internal pathname, but will be mapped to a locale-specific pathname. + +```tsx +import {Link} from '../navigation'; + +// When the user is on `/de`, the link will point to `/de/ueber-uns` +About + +// You can override the `locale` to switch to another language +Switch to English + +// Dynamic params need to be passed to a separate prop +Susan ``` -## `useRouter` + + + + + Note that `prefetch` will be turned off when linking to another locale to + avoid prematurely overwriting the locale cookie. + + +### `useRouter` 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(); @@ -33,8 +142,12 @@ const router = useRouter(); router.push('/about'); // You can override the `locale` to switch to another language -router.push('/about', {locale: 'de'}); -``` +router.replace('/about', {locale: 'de'}); + +// Dynamic params need to be interpolated into the pathname +router.push('/users/12', {locale: 'de'}); + +````
How can I change the locale for the current page? @@ -44,7 +157,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(); @@ -53,40 +166,129 @@ router.replace(pathname, {locale: 'de'}); ```
+
+ + +When using [localized pathnames](#shared-pathnames), the provided `href` corresponds to an internal pathname, but will be mapped to a locale-specific pathname. + +```tsx +'use client'; + +import {useRouter} from '../navigation'; + +const router = useRouter(); + +// When the user is on `/de`, the router will navigate to `/de/ueber-uns` +router.push('/about'); + +// You can override the `locale` to switch to another language +router.replace('/about', {locale: 'en'}); + +// Dynamic params need to be passed in object format +router.push({ + pathname: '/users/[userId]', + params: {userId: '12'} +}); +```` + +
+How can I change the locale for the current page? + +By combining [`usePathname`](#usepathname) with [`useRouter`](#userouter), you can change the locale for the current page programmatically. + +Note that if you have dynamic params on some routes, you should pass those as well to potentially resolve an internal pathname. + +```tsx +'use client'; + +import {useParams} from 'next/navigation'; +import {usePathname, useRouter} from '../navigation'; + +const pathname = usePathname(); +const router = useRouter(); +const params = useParams(); + +router.replace({pathname, params}, {locale: 'de'}); +``` + +
+
+
-## `usePathname` +### `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(); ``` -## `redirect` + + + +When using [localized pathnames](#shared-pathnames), the returned pathname will correspond to an internal pathname. + +```tsx +'use client'; + +import {usePathname} from '../navigation'; + +// When the user is on `/de/ueber-uns`, this will be `/about` +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 from `next-intl/server`. This wraps [the `redirect` function from Next.js](https://nextjs.org/docs/app/api-reference/functions/redirect) and automatically applies the current locale. +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 {1, 8} -import {redirect} from 'next-intl/server'; +```tsx +import {redirect} from '../navigation'; -export default async function Profile() { - const user = await fetchUser(); +// When the user is on `/en/profile`, this will be `/en/login` +redirect('/login'); - if (!user) { - // When the user is on `/en/profile`, this will be `/en/login` - redirect('/login'); - } +// Dynamic params need to be interpolated into the pathname +router.push('/users/12'); +``` - // ... -} + + + +When using [localized pathnames](#shared-pathnames), the provided `href` corresponds to an internal pathname, but will be mapped to a locale-specific pathname. + +```tsx +import {redirect} from '../navigation'; + +// When the user is on `/en/profile`, this will be `/en/login` +redirect('/login'); + +// Dynamic params need to be passed in object format +redirect({ + pathname: '/help/[articleSlug]', + params: {articleSlug: 'how-to-login'} +}); ``` + + + diff --git a/examples/example-next-13/src/components/LocaleSwitcherSelect.tsx b/examples/example-next-13/src/components/LocaleSwitcherSelect.tsx index 4c91488e5..9a4d4f6a5 100644 --- a/examples/example-next-13/src/components/LocaleSwitcherSelect.tsx +++ b/examples/example-next-13/src/components/LocaleSwitcherSelect.tsx @@ -17,12 +17,12 @@ export default function LocaleSwitcherSelect({ }: Props) { const router = useRouter(); const [isPending, startTransition] = useTransition(); - const namedPath = usePathname(); + const pathname = usePathname(); function onSelectChange(event: ChangeEvent) { const nextLocale = event.target.value; startTransition(() => { - router.replace(namedPath, {locale: nextLocale}); + router.replace(pathname, {locale: nextLocale}); }); } diff --git a/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx b/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx index e8279b758..1b241aefe 100644 --- a/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx +++ b/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx @@ -44,7 +44,7 @@ export default function createLocalizedPathnamesNavigation< href={compileLocalizedPathname({ locale: finalLocale, // @ts-expect-error -- No idea - href, + pathname: href, params, pathnames })} @@ -59,7 +59,7 @@ export default function createLocalizedPathnamesNavigation< type HrefOrHrefWithParams = | keyof Pathnames | { - href: keyof Pathnames; + pathname: keyof Pathnames; params?: Params; }; @@ -67,7 +67,7 @@ export default function createLocalizedPathnamesNavigation< nameOrNameWithParams: HrefOrHrefWithParams ) { return typeof nameOrNameWithParams === 'string' - ? {href: nameOrNameWithParams, params: undefined} + ? {pathname: nameOrNameWithParams, params: undefined} : nameOrNameWithParams; } diff --git a/packages/next-intl/src/navigation/react-server/createLocalizedPathnamesNavigation.tsx b/packages/next-intl/src/navigation/react-server/createLocalizedPathnamesNavigation.tsx index 307665877..923a544bf 100644 --- a/packages/next-intl/src/navigation/react-server/createLocalizedPathnamesNavigation.tsx +++ b/packages/next-intl/src/navigation/react-server/createLocalizedPathnamesNavigation.tsx @@ -25,7 +25,7 @@ export default function createLocalizedPathnamesNavigation< ; export function compileLocalizedPathname(opts: { locale: Locales[number]; // eslint-disable-next-line no-use-before-define -- False positive - href: keyof typeof opts.pathnames; + pathname: keyof typeof opts.pathnames; params?: Params; pathnames: Pathnames; }): string; export function compileLocalizedPathname(opts: { locale: Locales[number]; // eslint-disable-next-line no-use-before-define -- False positive - href: StrictUrlObject; + pathname: StrictUrlObject; params?: Params; pathnames: Pathnames; }): UrlObject; export function compileLocalizedPathname({ - href, + pathname, locale, params, pathnames }: { locale: Locales[number]; - href: keyof typeof pathnames | StrictUrlObject; + pathname: keyof typeof pathnames | StrictUrlObject; params?: Params; pathnames: Pathnames; }) { @@ -57,13 +57,13 @@ export function compileLocalizedPathname({ return compiled; } - if (typeof href === 'string') { - const namedPath = getNamedPath(href); + if (typeof pathname === 'string') { + const namedPath = getNamedPath(pathname); const compiled = compilePath(namedPath); return compiled; } else { - const {pathname, ...rest} = href; - const namedPath = getNamedPath(pathname); + const {pathname: href, ...rest} = pathname; + const namedPath = getNamedPath(href); const compiled = compilePath(namedPath); const result: UrlObject = {...rest, pathname: compiled}; return result; From 3704dd7497aa7290306a122c394280c736eeed1c Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Fri, 28 Jul 2023 17:51:14 +0200 Subject: [PATCH 10/37] Small fix --- .../createLocalizedPathnamesNavigation.tsx | 8 ++--- ...reateLocalizedPathnamesNavigation.test.tsx | 35 +++++++++++++++++-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx b/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx index 1b241aefe..d72bec007 100644 --- a/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx +++ b/packages/next-intl/src/navigation/createLocalizedPathnamesNavigation.tsx @@ -87,7 +87,7 @@ export default function createLocalizedPathnamesNavigation< function useRouter() { const baseRouter = useBaseRouter(); - const locale = useLocale(); + const defaultLocale = useLocale(); return { ...baseRouter, @@ -97,7 +97,7 @@ export default function createLocalizedPathnamesNavigation< ) { const href = compileLocalizedPathname({ ...normalizeNameOrNameWithParams(nameOrNameWithParams), - locale, + locale: args[0]?.locale || defaultLocale, pathnames }); return baseRouter.push(href, ...args); @@ -109,7 +109,7 @@ export default function createLocalizedPathnamesNavigation< ) { const href = compileLocalizedPathname({ ...normalizeNameOrNameWithParams(nameOrNameWithParams), - locale, + locale: args[0]?.locale || defaultLocale, pathnames }); return baseRouter.replace(href, ...args); @@ -121,7 +121,7 @@ export default function createLocalizedPathnamesNavigation< ) { const href = compileLocalizedPathname({ ...normalizeNameOrNameWithParams(nameOrNameWithParams), - locale, + locale: args[0]?.locale || defaultLocale, pathnames }); return baseRouter.prefetch(href, ...args); diff --git a/packages/next-intl/test/navigation/createLocalizedPathnamesNavigation.test.tsx b/packages/next-intl/test/navigation/createLocalizedPathnamesNavigation.test.tsx index 3d5d9cd92..cab7fcc1d 100644 --- a/packages/next-intl/test/navigation/createLocalizedPathnamesNavigation.test.tsx +++ b/packages/next-intl/test/navigation/createLocalizedPathnamesNavigation.test.tsx @@ -1,12 +1,16 @@ import {render, screen} from '@testing-library/react'; -import {usePathname, useParams} from 'next/navigation'; +import { + usePathname, + useParams, + useRouter as useNextRouter +} from 'next/navigation'; import React from 'react'; -import {it, describe, vi, beforeEach, expect} from 'vitest'; +import {it, describe, vi, beforeEach, expect, Mock} from 'vitest'; import {createLocalizedPathnamesNavigation} from '../../src/navigation'; vi.mock('next/navigation'); -const {Link} = createLocalizedPathnamesNavigation({ +const {Link, useRouter} = createLocalizedPathnamesNavigation({ locales: ['en', 'de'], pathnames: { '/': '/', @@ -22,6 +26,15 @@ const {Link} = createLocalizedPathnamesNavigation({ }); 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'})); }); @@ -67,3 +80,19 @@ describe('Link', () => { ); }); }); + +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'); + }); + }); +}); From 972308042e20a266324bf7663a16517d5d155ea1 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Mon, 31 Jul 2023 11:11:40 +0200 Subject: [PATCH 11/37] Improve docs and convention --- docs/pages/docs/routing/index.mdx | 6 +- docs/pages/docs/routing/middleware.mdx | 19 +-- docs/pages/docs/routing/navigation.mdx | 161 ++++++++++++------ docs/pages/index.mdx | 2 +- .../example-next-13-advanced/src/config.tsx | 20 --- .../src/middleware.ts | 2 +- .../src/navigation.tsx | 25 ++- examples/example-next-13/src/i18n.ts | 13 -- examples/example-next-13/src/middleware.ts | 2 +- examples/example-next-13/src/navigation.ts | 20 +++ examples/example-next-13/src/navigation.tsx | 22 --- 11 files changed, 167 insertions(+), 125 deletions(-) delete mode 100644 examples/example-next-13-advanced/src/config.tsx create mode 100644 examples/example-next-13/src/navigation.ts delete mode 100644 examples/example-next-13/src/navigation.tsx diff --git a/docs/pages/docs/routing/index.mdx b/docs/pages/docs/routing/index.mdx index a37790a70..d9732afad 100644 --- a/docs/pages/docs/routing/index.mdx +++ b/docs/pages/docs/routing/index.mdx @@ -3,13 +3,13 @@ import {Card} from 'nextra-theme-docs'; # Internationalized routing -With the introduction of the App Router, Next.js no longer provides integrated i18n routing. To fill in the gap, `next-intl` provides the two necessary pieces: +When you provide content in multiple languages, you want to make the content available under distinct URLs (e.g. `ca.example.com/en/about`). `next-intl` provides the building blocks to set up internatinoalized routing as well as the navigation APIs to enable you to link between pages.
} - title="Internationalized routing middleware" + title="Routing middleware" href="/docs/routing/middleware" />
+ +Note that these features are only relevant if you use the App Router. If you're using [`next-intl` with the Pages Router](/docs/getting-started/pages-router), you can use the [built-in capabilities from Next.js](https://nextjs.org/docs/pages/building-your-application/routing/internationalization). diff --git a/docs/pages/docs/routing/middleware.mdx b/docs/pages/docs/routing/middleware.mdx index 3b75852c9..3da552356 100644 --- a/docs/pages/docs/routing/middleware.mdx +++ b/docs/pages/docs/routing/middleware.mdx @@ -22,6 +22,8 @@ export const config = { }; ``` +In addition to handling i18n routing, the middleware sets [the `link` header](https://developers.google.com/search/docs/specialty/international/localized-versions#http) to inform search engines that your content is available in different languages. + ## Strategies There are two strategies for detecting the locale: @@ -189,7 +191,7 @@ In this case, only the locale prefix and a potentially [matching domain](#domain The middleware automatically sets [the `link` header](https://developers.google.com/search/docs/specialty/international/localized-versions#http) to inform search engines that your content is available in different languages. Note that this automatically integrates with your routing strategy and will generate the correct links based on your configuration. -If you prefer to include these links yourself, e.g. because you're using [locale-specific rewrites](#localizing-pathnames), you can opt-out of this behavior. +If you prefer to include these links yourself, you can opt-out of this behavior. ```tsx filename="middleware.ts" {6} import createMiddleware from 'next-intl/middleware'; @@ -208,9 +210,9 @@ Many apps choose to localize pathnames, especially when search engine optimizati - `/en/about` - `/de/ueber-uns` -Since you want to define these routes only once internally, you can use the `next-intl` middleware to [rewrite](https://nextjs.org/docs/api-reference/next.config.js/rewrites) such incoming requests to shared paths internally. +Since you want to define these routes only once internally, you can use the `next-intl` middleware to [rewrite](https://nextjs.org/docs/api-reference/next.config.js/rewrites) such incoming requests to shared pathnames. -```js filename="middleware.ts" +```tsx filename="middleware.ts" import createMiddleware from 'next-intl/middleware'; export default createMiddleware({ @@ -221,7 +223,7 @@ export default createMiddleware({ // external paths. Based on the locale, the external // paths are rewritten to the shared, internal ones. pathnames: { - // If all locales use the same paths, a single + // If all locales use the same pathname, a single // external path can be used for all locales. '/': '/', '/blog': '/blog', @@ -242,13 +244,10 @@ export default createMiddleware({ }); ``` -Note that the middleware will adapt the returned [alternate links](http://localhost:3000/docs/routing/middleware#disable-alternate-links) depending on this configuration. - - If you have pathname localization set up in your middleware, you likely want - to use the [localized navigation - APIs](/docs/routing/navigation#strategy-2-localized-pathnames) in your - components. + If you have pathname localization set up in the middleware, you likely want to + use the [localized navigation + APIs](/docs/routing/navigation#localized-pathnames) in your components. ## Composing other middlewares diff --git a/docs/pages/docs/routing/navigation.mdx b/docs/pages/docs/routing/navigation.mdx index 1212ac9a0..bdccd1608 100644 --- a/docs/pages/docs/routing/navigation.mdx +++ b/docs/pages/docs/routing/navigation.mdx @@ -7,76 +7,105 @@ import {Tab, Tabs} from 'nextra-theme-docs'; ## Strategies -There are two strategies that you can use based on your needs: +There are two strategies that you can use based on your needs. -1. [Shared pathnames](#shared-pathnames) -2. [Localized pathnames](#localized-pathnames) +The simplest case is when your app uses [shared pathnames](#shared-pathnames), regardless of the locale, e.g.: -Each strategy will provide you with corresponding [navigation APIs](#apis) that you can use in your components. Typically, you'll provide these APIs in a central module that you can easily reference (e.g. `./navigation.ts`). - -### Strategy 1: Shared pathnames [#shared-pathnames] +- `/en/about` +- `/de/about` -The simplest case is when your app uses the same pathnames, regardless of the locale, e.g.: +Many apps choose to [localize pathnames](#localized-pathnames) however, especially when search engine optimization is relevant. In this case, you'll provide distinct pathnames based on the user locale, e.g.: - `/en/about` -- `/en-GB/about` +- `/de/ueber-uns` + +Each strategy will provide you with corresponding [navigation APIs](#apis) that you'll typically provide in a central module to easily access them in components (e.g. `./navigation.ts`). + +### Strategy 1: Shared pathnames [#shared-pathnames] -In this example, the path segment `about` remains the same, regardless of the user's locale. This is the simplest case, because the routes you define in Next.js will map directly to the URLs that a user can request. +With this strategy, the pathnames 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. -To create [navigation APIs](#apis) for this use case, use the `createSharedPathnamesNavigation` function: +To create [navigation APIs](#apis) for this strategy, use the `createSharedPathnamesNavigation` function: -```tsx filename="./navigation.tsx" +```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: ['en', 'en-GB'], - pathnames: { - '/': '/', - '/about': { - en: '/about', - de: '/ueber-uns' - } - } - }); + 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. + +```tsx filename="middleware.ts" +import createMiddleware from 'next-intl/middleware'; +import {locales} from './navigation'; + +export default createMiddleware({ + defaultLocale: 'en', + locales +}); ``` -The `pathnames` argument is identical to the configuration that you pass to the middleware for [localizing pathnames](/docs/routing/middleware#localizing-pathnames). Because of this, you might want to share it along with the `locales` from a shared file like `i18n.ts`: +### Strategy 2: Localized pathnames [#localized-pathnames] + +When using this strategy, you have to provide distinct pathnames for every locale that your app supports. However, the localized variants should be handled by a single route internally, therefore a mapping needs to be provided that is [consumed by the middleware too](/docs/routing/middleware#localizing-pathnames). + +You can use the `createLocalizedPathnamesNavigation` function to create corresponding [navigation APIs](#apis): -```tsx filename="i18n.ts" -import {Pathnames} from 'next-intl/navigation'; +```tsx filename="./navigation.ts" +import { + createLocalizedPathnamesNavigation, + Pathnames +} from 'next-intl/navigation'; export const locales = ['en', 'de'] as const; +// The `pathnames` object holds pairs of internal +// and external paths. Based on the locale, the +// corresponding external path will be picked. export const pathnames: Pathnames = { + // If all locales use the same pathname, a single + // external path can be used for all locales. '/': '/', + '/blog': '/blog', + + // If locales use different paths, you can + // specify each external path per locale. '/about': { en: '/about', de: '/ueber-uns' + }, + + // Dynamic params are supported via square brackets + '/news/[articleSlug]-[articleId]': { + en: '/news/[articleSlug]-[articleId]', + de: '/neuigkeiten/[articleSlug]-[articleId]' } } as const; -``` - -Have a look at the [App Router example](/examples/app-router) to explore a working implementation of localized pathnames. - -### Strategy 2: Localized pathnames [#localized-pathnames] -If you've configured the middleware to [use localized pathnames](/docs/routing/middleware#localizing-pathnames), you can use the `createLocalizedPathnamesNavigation` function to create corresponding navigation APIs: - -'re using the middleware configuration localized pathnames in the +export const {Link, redirect, usePathname, useRouter} = + createLocalizedPathnamesNavigation({locales, pathnames}); +``` -Many apps choose to localize pathnames, especially when search engine optimization is relevant: +The `pathnames` argument is identical to the configuration that you pass to the middleware for [localizing pathnames](/docs/routing/middleware#localizing-pathnames). Because of this, you might want to import the `locales` and `pathnames` into the middleware. -```tsx filename="./navigation.tsx" -import {createLocalizedPathnamesNavigation} from 'next-intl/navigation'; +```tsx filename="middleware.ts" +import createMiddleware from 'next-intl/middleware'; +import {locales, pathnames} from './navigation'; -export const {Link, redirect, usePathname, useRouter} = - createLocalizedPathnamesNavigation({ - locales: ['en', 'en-GB'] - }); +export default createMiddleware({ + defaultLocale: 'en', + locales, + pathnames +}); ``` ---- + + Have a look at the [App Router example](/examples/app-router) to explore a + working implementation of localized pathnames. + ## APIs @@ -102,7 +131,7 @@ import {Link} from '../navigation'; -When using [localized pathnames](#shared-pathnames), the `href` prop corresponds to an internal pathname, but will be mapped to a locale-specific pathname. +When using [localized pathnames](#localized-pathnames), the `href` prop corresponds to an internal pathname, but will be mapped to a locale-specific pathname. ```tsx import {Link} from '../navigation'; @@ -113,17 +142,43 @@ import {Link} from '../navigation'; // You can override the `locale` to switch to another language Switch to English -// Dynamic params need to be passed to a separate prop +// Dynamic params need to be passed separately Susan ``` - - Note that `prefetch` will be turned off when linking to another locale to - avoid prematurely overwriting the locale cookie. - +
+How can I render a navigation link? + +The [`useSelectedLayoutSegment` hook](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment) from Next.js allows you to detect if a given child segment is active from within the parent layout. Since this returns an internal pathname, it can be matched against an `href` that you can pass to `Link`. + +```tsx +function BlogNavigationLink() { + const href = '/blog'; + + const selectedLayoutSegment = useSelectedLayoutSegment(); + const activeHref = selectedLayoutSegment ? `/${selectedLayoutSegment}` : '/'; + const isActive = activeHref === href; + + return ( + + Blog + + ); +} +``` + +See also the Next.js docs on [creating an active link component](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment#creating-an-active-link-component). + +
+ +
+How does prefetching of localized links work? +Just like `next/link`, by default all links are prefetched. The one exception to this is that links to other locales aren't prefetched, because this would result in prematurely overwriting the locale cookie. + +
### `useRouter` @@ -169,7 +224,7 @@ router.replace(pathname, {locale: 'de'}); -When using [localized pathnames](#shared-pathnames), the provided `href` corresponds to an internal pathname, but will be mapped to a locale-specific pathname. +When using [localized pathnames](#localized-pathnames), the provided `href` corresponds to an internal pathname, but will be mapped to a locale-specific pathname. ```tsx 'use client'; @@ -184,7 +239,7 @@ router.push('/about'); // You can override the `locale` to switch to another language router.replace('/about', {locale: 'en'}); -// Dynamic params need to be passed in object format +// Dynamic params need to be provided as objects router.push({ pathname: '/users/[userId]', params: {userId: '12'} @@ -234,7 +289,7 @@ const pathname = usePathname(); -When using [localized pathnames](#shared-pathnames), the returned pathname will correspond to an internal pathname. +When using [localized pathnames](#localized-pathnames), the returned pathname will correspond to an internal pathname. ```tsx 'use client'; @@ -265,7 +320,7 @@ If you want to interrupt the render and redirect to another page, you can invoke ```tsx import {redirect} from '../navigation'; -// When the user is on `/en/profile`, this will be `/en/login` +// When the user is on `/en`, this will be `/en/login` redirect('/login'); // Dynamic params need to be interpolated into the pathname @@ -275,15 +330,15 @@ router.push('/users/12'); -When using [localized pathnames](#shared-pathnames), the provided `href` corresponds to an internal pathname, but will be mapped to a locale-specific pathname. +When using [localized pathnames](#localized-pathnames), the provided `href` corresponds to an internal pathname, but will be mapped to a locale-specific pathname. ```tsx import {redirect} from '../navigation'; -// When the user is on `/en/profile`, this will be `/en/login` +// When the user is on `/en`, this will be `/en/login` redirect('/login'); -// Dynamic params need to be passed in object format +// Dynamic params need to be provided as objects redirect({ pathname: '/help/[articleSlug]', params: {articleSlug: 'how-to-login'} diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx index f4cd3cacf..eeb1a826d 100644 --- a/docs/pages/index.mdx +++ b/docs/pages/index.mdx @@ -24,7 +24,7 @@ import Hero from 'components/Hero';
diff --git a/examples/example-next-13-advanced/src/config.tsx b/examples/example-next-13-advanced/src/config.tsx deleted file mode 100644 index 4466c4e9c..000000000 --- a/examples/example-next-13-advanced/src/config.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import {Pathnames} from 'next-intl/navigation'; - -export const locales = ['en', 'de', 'es'] as const; - -export const pathnames: 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]' - } -} as const; diff --git a/examples/example-next-13-advanced/src/middleware.ts b/examples/example-next-13-advanced/src/middleware.ts index 4e7a42eeb..b5529e52b 100644 --- a/examples/example-next-13-advanced/src/middleware.ts +++ b/examples/example-next-13-advanced/src/middleware.ts @@ -1,5 +1,5 @@ import createMiddleware from 'next-intl/middleware'; -import {locales, pathnames} from './config'; +import {locales, pathnames} from './navigation'; export default createMiddleware({ defaultLocale: 'en', diff --git a/examples/example-next-13-advanced/src/navigation.tsx b/examples/example-next-13-advanced/src/navigation.tsx index d98738ffe..4b78e8cca 100644 --- a/examples/example-next-13-advanced/src/navigation.tsx +++ b/examples/example-next-13-advanced/src/navigation.tsx @@ -1,5 +1,26 @@ -import {createLocalizedPathnamesNavigation} from 'next-intl/navigation'; -import {locales, pathnames} from './config'; +import { + createLocalizedPathnamesNavigation, + Pathnames +} from 'next-intl/navigation'; + +export const locales = ['en', 'de', 'es'] as const; + +export const pathnames: 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]' + } +} as const; export const {Link, redirect, usePathname, useRouter} = createLocalizedPathnamesNavigation({ diff --git a/examples/example-next-13/src/i18n.ts b/examples/example-next-13/src/i18n.ts index d1a637dbd..c45ddee14 100644 --- a/examples/example-next-13/src/i18n.ts +++ b/examples/example-next-13/src/i18n.ts @@ -1,18 +1,5 @@ -import {Pathnames} from 'next-intl/navigation'; import {getRequestConfig} from 'next-intl/server'; -export const locales = ['en', 'de'] as const; - -// is it good that export this here? do we need to worry about tree shaking to include server-only code? -// try in advanced example -export const pathnames: Pathnames = { - '/': '/', - '/about': { - en: '/about', - de: '/ueber' - } -} as const; - export default getRequestConfig(async ({locale}) => ({ 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 index fdad3a20b..dde608338 100644 --- a/examples/example-next-13/src/middleware.ts +++ b/examples/example-next-13/src/middleware.ts @@ -1,5 +1,5 @@ import createMiddleware from 'next-intl/middleware'; -import {pathnames, locales} from './i18n'; +import {pathnames, locales} from './navigation'; export default createMiddleware({ defaultLocale: 'en', diff --git a/examples/example-next-13/src/navigation.ts b/examples/example-next-13/src/navigation.ts new file mode 100644 index 000000000..5bb06edc2 --- /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: Pathnames = { + '/': '/', + '/about': { + en: '/about', + de: '/ueber' + } +} as const; + +export const {Link, redirect, usePathname, useRouter} = + createLocalizedPathnamesNavigation({ + locales, + pathnames + }); diff --git a/examples/example-next-13/src/navigation.tsx b/examples/example-next-13/src/navigation.tsx deleted file mode 100644 index ec3ce5aaf..000000000 --- a/examples/example-next-13/src/navigation.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import {createLocalizedPathnamesNavigation} from 'next-intl/navigation'; -import {locales, pathnames} from './i18n'; - -export const {Link, redirect, usePathname, useRouter} = - createLocalizedPathnamesNavigation({ - locales, - pathnames - }); - -// export const {Link, redirect, useRouter} = createDefaultNavigation({ -// locales, -// pathnames -// }); - -// this has some advantages: -// - no type overloading for redirect and router push -// - strong types for the locale and default locale -// - usePathname - -// can these apis be used in both server and client files? -// maybe we need to fork createNamedRouting into a react-server version -// that could work, because we have to separate bundles From d76904e8694970afaefffbb96d62187ba99dd3a8 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Mon, 31 Jul 2023 12:34:47 +0200 Subject: [PATCH 12/37] Upgrade ESLint --- docs/.eslintrc.js | 3 +- docs/components/Callout.tsx | 2 +- docs/components/CodeSnippets.tsx | 4 +- docs/components/CommunityLink.tsx | 2 +- docs/components/Footer.tsx | 2 +- docs/components/Hero.tsx | 4 +- docs/components/HeroCode.tsx | 2 +- docs/components/LinkButton.tsx | 2 +- docs/package.json | 6 +- docs/pages/docs/routing/middleware.mdx | 3 +- docs/prettier.config.js | 3 +- examples/example-advanced/.eslintrc.js | 2 - examples/example-advanced/package.json | 4 +- .../src/components/Navigation.spec.tsx | 2 - .../example-next-13-advanced/.eslintrc.js | 2 - .../example-next-13-advanced/package.json | 4 +- .../example-next-13-named-routes/.eslintrc.js | 2 - .../example-next-13-named-routes/package.json | 4 +- .../example-next-13-next-auth/.eslintrc.js | 2 - .../example-next-13-next-auth/package.json | 4 +- .../example-next-13-with-pages/.eslintrc.js | 2 - .../example-next-13-with-pages/package.json | 4 +- examples/example-next-13/.eslintrc.js | 3 +- examples/example-next-13/package.json | 6 +- examples/example-next-13/prettier.config.js | 3 - .../src/components/LocaleSwitcherSelect.tsx | 2 +- .../src/components/Navigation.tsx | 2 +- .../src/components/NavigationLink.tsx | 2 +- .../src/components/PageLayout.tsx | 2 +- examples/example-remix/.eslintrc.js | 2 - examples/example-remix/package.json | 4 +- examples/example/.eslintrc.js | 2 - examples/example/package.json | 4 +- packages/next-intl/.eslintrc.js | 2 - packages/next-intl/package.json | 4 +- packages/use-intl/.eslintrc.js | 2 - packages/use-intl/package.json | 4 +- .../src/core/createBaseTranslator.tsx | 2 +- .../use-intl/src/core/createTranslator.tsx | 1 - .../use-intl/src/react/useTranslations.tsx | 2 +- .../use-intl/test/react/useFormatter.test.tsx | 2 +- packages/use-intl/test/react/useIntl.test.tsx | 2 +- pnpm-lock.yaml | 1111 +++++++++-------- 43 files changed, 642 insertions(+), 587 deletions(-) delete mode 100644 examples/example-next-13/prettier.config.js diff --git a/docs/.eslintrc.js b/docs/.eslintrc.js index b7d452735..fdca848ef 100644 --- a/docs/.eslintrc.js +++ b/docs/.eslintrc.js @@ -1,9 +1,8 @@ -require('eslint-config-molindo/setupPlugins'); - module.exports = { extends: [ 'molindo/typescript', 'molindo/react', + 'molindo/tailwind', 'plugin:@next/next/recommended' ], env: { diff --git a/docs/components/Callout.tsx b/docs/components/Callout.tsx index 46d7bcb05..7fa63cc5a 100644 --- a/docs/components/Callout.tsx +++ b/docs/components/Callout.tsx @@ -44,7 +44,7 @@ export default function Callout({ )} >
{"''"} -
-
+
+
title
followers
diff --git a/docs/components/CommunityLink.tsx b/docs/components/CommunityLink.tsx index 81f481d33..c8abe88ea 100644 --- a/docs/components/CommunityLink.tsx +++ b/docs/components/CommunityLink.tsx @@ -23,7 +23,7 @@ export default function CommunityLink({
{type && ( {{article: 'Article', video: 'Video'}[type]} diff --git a/docs/components/Footer.tsx b/docs/components/Footer.tsx index 0a73de5ba..69b88ebc1 100644 --- a/docs/components/Footer.tsx +++ b/docs/components/Footer.tsx @@ -11,7 +11,7 @@ export default function Footer() { return (
-
+
Docs diff --git a/docs/components/Hero.tsx b/docs/components/Hero.tsx index cd9e645e0..c99fceb37 100644 --- a/docs/components/Hero.tsx +++ b/docs/components/Hero.tsx @@ -22,8 +22,8 @@ export default function Hero({ }: Props) { return (
-
-
+
+
diff --git a/docs/components/HeroCode.tsx b/docs/components/HeroCode.tsx index 44e9405da..bf7a05404 100644 --- a/docs/components/HeroCode.tsx +++ b/docs/components/HeroCode.tsx @@ -16,7 +16,7 @@ function Tab({ className={clsx( 'flex items-center rounded-md px-4 py-2 text-sm font-medium transition-colors', active - ? 'bg-slate-800 text-sky-100/70 text-white' + ? 'bg-slate-800 text-white' : 'bg-slate-800/40 text-slate-500 hover:bg-slate-800' )} onClick={onClick} diff --git a/docs/components/LinkButton.tsx b/docs/components/LinkButton.tsx index 6fc2ea847..e94bd692a 100644 --- a/docs/components/LinkButton.tsx +++ b/docs/components/LinkButton.tsx @@ -10,7 +10,7 @@ export default function LinkButton({variant = 'primary', ...rest}: Props) { return ( {children} - + ); } diff --git a/examples/example-next-13/src/components/Navigation.tsx b/examples/example-next-13/src/components/Navigation.tsx index 809b25ce2..0f286160b 100644 --- a/examples/example-next-13/src/components/Navigation.tsx +++ b/examples/example-next-13/src/components/Navigation.tsx @@ -7,7 +7,7 @@ export default function Navigation() { return (
-