import { normalizePathTrailingSlash } from 'next/dist/client/normalize-trailing-slash' import { parse as parsePathPattern, compile as compilePath } from 'path-to-regexp' import { format as formatUrl, UrlObject } from 'url' import { ignoreSegmentPathRegex } from '../shared/regex' import { ntrMessagePrefix } from '../shared/withNtrPrefix' import type { TRouteBranch } from '../types' import { getNtrData } from './ntrData' import { urlToUrlObject } from './urlToUrlObject' const getPatternFromRoutePaths = (routeBranch: TRouteBranch, locale: string) => { const pattern = routeBranch.paths[locale] || routeBranch.paths.default return ignoreSegmentPathRegex.test(pattern) ? '' : pattern } /** Add `/` prefix only if `pattern` is not an empty string and does not already have it */ const addSlashPrefix = (path: string) => (path === '' || path.startsWith('/') ? path : `/${path}`) const getPathPatternPart = ({ routeBranch, locale, isLastPathPart, }: { routeBranch: TRouteBranch locale: string isLastPathPart: boolean }) => { let pattern = getPatternFromRoutePaths(routeBranch, locale) if (isLastPathPart) { if (routeBranch.children?.length) { const indexChild = routeBranch.children.find((child) => child.name === 'index' && !child.children?.length) if (!indexChild) { throw new Error(`No index file found in "${routeBranch.name}" folder.`) } const indexChildPattern = getPatternFromRoutePaths(indexChild, locale) if (indexChildPattern && indexChildPattern !== 'index') { pattern = pattern + addSlashPrefix(indexChildPattern) } } else if (pattern === 'index' && routeBranch.name === 'index') { pattern = '' } } return addSlashPrefix(pattern) } /** * Get path pattern (path-to-regexp synthax) from file path parts * * Ex: `/[dynamic]/path` => `/:dynamic/path` */ export const getTranslatedPathPattern = ({ routeBranch, pathParts, locale, }: { routeBranch: TRouteBranch /** Remaining path parts after the `routeBranch` path parts */ pathParts: string[] locale: string }): string => { const isLastPathPart = pathParts.length === 0 const currentPathPatternPart = getPathPatternPart({ routeBranch, locale, isLastPathPart }) if (isLastPathPart) { return currentPathPatternPart } const nextPathPart = pathParts[0] const remainingPathParts = pathParts.slice(1) for (const child of routeBranch.children || []) { if ( // child name must match nextPathPart child.name === nextPathPart && // child.children must be coherent with remaining path parts is case a file and and folder share the same name (remainingPathParts.length === 0 || child.children?.length) ) { return ( currentPathPatternPart + getTranslatedPathPattern({ routeBranch: child, pathParts: remainingPathParts, locale, }) ) } } throw new Error(`No "/${pathParts.join('/')}" page found in /${routeBranch.name} folder.`) } /** * Translate Next file url into translated string url * * @param url Next default file url * @param locale string */ export const fileUrlToUrl = (url: UrlObject | URL | string, locale: string) => { const { routesTree, defaultLocale } = getNtrData() const { pathname, query, hash } = urlToUrlObject(url) if (!routesTree.children) { throw new Error('No page found. You probably need to add the pageDirectory option in your translateRoutes config.') } try { const pathParts = (pathname || '/') .replace(/^\/|\/$/g, '') .split('/') .filter(Boolean) // Create a new query object that we can mutate const newQuery = { ...query } const pathPattern = getTranslatedPathPattern({ routeBranch: routesTree, pathParts, locale }) const newPathname = normalizePathTrailingSlash(compilePath(pathPattern)(query)) for (const patterToken of parsePathPattern(pathPattern)) { if (typeof patterToken === 'object' && patterToken.name) { delete newQuery[patterToken.name] } } const hasLocalePrefix = locale !== defaultLocale && process.env.NEXT_PUBLIC_HAS_LOCALE_PREFIX !== 'false' return `${hasLocalePrefix ? `/${locale}` : ''}${formatUrl({ pathname: newPathname, query: newQuery, hash, })}` } catch (error) { throw new Error(ntrMessagePrefix + `No page found for pathname ${pathname}`, { cause: error }) } }