Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions example/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ const nextConfig = withTranslateRoutes({
i18n: {
locales: ['en', 'fr'],
defaultLocale: 'en',
localeDetection: false,
domains: [
{
domain: 'localhost:3000',
defaultLocale: 'fr',
http: true,
},
{
domain: 'localhost:3001',
defaultLocale: 'en',
http: true,
},
],
},

translateRoutes: {
Expand Down
5 changes: 2 additions & 3 deletions src/plugin/createNtrData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { parsePages } from './parsePages'
export const createNtrData = (nextConfig: NextConfig, customPagesPath?: string): TNtrData => {
const {
pageExtensions = ['js', 'ts', 'jsx', 'tsx'],
i18n: { defaultLocale, locales = [] },
i18n,
translateRoutes: { debug, routesDataFileName, routesTree: customRoutesTree, pagesDirectory } = {},
} = nextConfig as NextConfigWithNTR
const pagesPath = customPagesPath || getPagesPath(pagesDirectory)
Expand All @@ -17,8 +17,7 @@ export const createNtrData = (nextConfig: NextConfig, customPagesPath?: string):

return {
debug,
defaultLocale,
locales,
i18n,
routesTree,
}
}
Expand Down
45 changes: 16 additions & 29 deletions src/plugin/getRouteBranchReRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Redirect, Rewrite } from 'next/dist/lib/load-custom-routes'
import { pathToRegexp } from 'path-to-regexp'

import { getNtrData } from '../react/ntrData'
import { isDefaultLocale } from '../shared/isDefaultLocale'
import { ignoreSegmentPathRegex } from '../shared/regex'
import type { TReRoutes, TRouteBranch, TRouteSegment } from '../types'
import { fileNameToPath } from './fileNameToPaths'
Expand Down Expand Up @@ -49,22 +51,14 @@ const mergeOrRegex = (existingRegex: string, newPossiblity: string) => {
/**
* Get redirects and rewrites for a page
*/
export const getPageReRoutes = <L extends string>({
locales,
routeSegments,
defaultLocale,
}: {
locales: L[]
routeSegments: TRouteSegment<L>[]
defaultLocale?: L
}): TReRoutes => {
export const getPageReRoutes = ({ routeSegments }: { routeSegments: TRouteSegment[] }): TReRoutes => {
/** If there is only one path possible: it is common to all locales and to files. No redirection nor rewrite is needed. */
if (!routeSegments.some(({ paths }) => Object.keys(paths).length > 1)) {
return { rewrites: [], redirects: [] }
}

/** Get a translated path or base path */
const getPath = (locale: L | 'default') =>
const getPath = (locale: string) =>
`/${routeSegments
.map(({ paths }) => paths[locale] || paths.default)
.filter((pathPart) => pathPart && !ignoreSegmentPathRegex.test(pathPart))
Expand All @@ -80,6 +74,8 @@ export const getPageReRoutes = <L extends string>({
.filter(Boolean) // Filter out falsy values
.join('/')}`

const { i18n } = getNtrData()

/**
* ```
* [
Expand All @@ -90,7 +86,7 @@ export const getPageReRoutes = <L extends string>({
* ```
* Each locale cannot appear more than once. Item is ignored if its path would be the same as basePath.
*/
const sourceList = locales.reduce((acc, locale) => {
const sourceList = i18n.locales.reduce((acc, locale) => {
const source = getPath(locale)
if (source === basePath) {
return acc
Expand All @@ -100,11 +96,11 @@ export const getPageReRoutes = <L extends string>({
...acc.filter((sourceItem) => sourceItem.source !== source),
{ source, sourceLocales: [...sourceLocales, locale] },
]
}, [] as { sourceLocales: L[]; source: string }[])
}, [] as { sourceLocales: string[]; source: string }[])

const redirects = locales.reduce((acc, locale) => {
const redirects = i18n.locales.reduce((acc, locale) => {
const localePath = getPath(locale)
const destination = `${locale === defaultLocale ? '' : `/${locale}`}${sourceToDestination(localePath)}`
const destination = `${isDefaultLocale(locale, i18n) ? '' : `/${locale}`}${sourceToDestination(localePath)}`

return [
...acc,
Expand Down Expand Up @@ -203,16 +199,12 @@ export const getPageReRoutes = <L extends string>({
/**
* Generate reroutes in route branch to feed the rewrite section of next.config
*/
export const getRouteBranchReRoutes = <L extends string>({
locales,
export const getRouteBranchReRoutes = ({
routeBranch: { children, ...routeSegment },
previousRouteSegments = [],
defaultLocale,
}: {
locales: L[]
routeBranch: TRouteBranch<L>
previousRouteSegments?: TRouteSegment<L>[]
defaultLocale?: L
routeBranch: TRouteBranch
previousRouteSegments?: TRouteSegment[]
}): TReRoutes => {
const routeSegments = [...previousRouteSegments, routeSegment]

Expand All @@ -221,19 +213,14 @@ export const getRouteBranchReRoutes = <L extends string>({
(acc, child) => {
const childReRoutes =
child.name === 'index'
? getPageReRoutes({ locales, routeSegments, defaultLocale })
: getRouteBranchReRoutes({
locales,
routeBranch: child,
previousRouteSegments: routeSegments,
defaultLocale,
})
? getPageReRoutes({ routeSegments })
: getRouteBranchReRoutes({ routeBranch: child, previousRouteSegments: routeSegments })
return {
redirects: [...acc.redirects, ...childReRoutes.redirects],
rewrites: [...acc.rewrites, ...childReRoutes.rewrites],
}
},
{ redirects: [], rewrites: [] } as TReRoutes,
)
: getPageReRoutes({ locales, routeSegments, defaultLocale })
: getPageReRoutes({ routeSegments })
}
16 changes: 6 additions & 10 deletions src/plugin/parsePages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,14 @@ import { fileNameToPath } from './fileNameToPaths'
import { getPagesDir, isRoutesFileName } from './routesFiles'

/** Get path and path translations from name and all translations #childrenOrder */
const getRouteSegment = <L extends string>(
name: string,
routeSegmentsData: TRouteSegmentsData<L>,
isDirectory?: boolean,
): TRouteSegment<L> => {
const getRouteSegment = (name: string, routeSegmentsData: TRouteSegmentsData, isDirectory?: boolean): TRouteSegment => {
const routeSegmentData = routeSegmentsData?.[isDirectory ? '/' : name]
const { default: defaultPath = fileNameToPath(name), ...localized } =
typeof routeSegmentData === 'object' ? routeSegmentData : { default: routeSegmentData }
const paths = {
default: defaultPath,
...localized,
} as TRouteSegmentPaths<L>
} as TRouteSegmentPaths
return {
name,
paths,
Expand Down Expand Up @@ -49,12 +45,12 @@ export type TParsePageTreeProps = {
/**
* Recursively parse pages directory and build a page tree object
*/
export const parsePages = <L extends string>({
export const parsePages = ({
directoryPath: propDirectoryPath,
pageExtensions,
isSubBranch,
routesDataFileName,
}: TParsePageTreeProps): TRouteBranch<L> => {
}: TParsePageTreeProps): TRouteBranch => {
const directoryPath = propDirectoryPath || getPagesDir()
const directoryItems = fs.readdirSync(directoryPath)
const routesFileName = directoryItems.find((directoryItem) => isRoutesFileName(directoryItem, routesDataFileName))
Expand All @@ -65,7 +61,7 @@ export const parsePages = <L extends string>({
routeSegmentsFileContent
? (/\.yaml$/.test(routesFileName as string) ? YAML : JSON).parse(routeSegmentsFileContent)
: {}
) as TRouteSegmentsData<L>
) as TRouteSegmentsData
const directoryPathParts = directoryPath.replace(/[\\/]/, '').split(/[\\/]/)
const name = isSubBranch ? directoryPathParts[directoryPathParts.length - 1] : ''

Expand Down Expand Up @@ -93,7 +89,7 @@ export const parsePages = <L extends string>({
]
}
return acc
}, [] as TRouteBranch<L>[])
}, [] as TRouteBranch[])
.sort((childA, childB) => getOrderWeight(childA) - getOrderWeight(childB))

return {
Expand Down
9 changes: 5 additions & 4 deletions src/plugin/withTranslateRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import type { Redirect, Rewrite } from 'next/dist/lib/load-custom-routes'
import type { NextConfig } from 'next/dist/server/config-shared'
import type { Configuration as WebpackConfiguration, FileCacheOptions } from 'webpack'

import { setNtrData } from '../react/ntrData'
import { ntrMessagePrefix } from '../shared/withNtrPrefix'
import { NextConfigWithNTR } from '../types'
import type { NextConfigWithNTR } from '../types'
import { createNtrData } from './createNtrData'
import { getPagesPath } from './getPagesPath'
import { getRouteBranchReRoutes } from './getRouteBranchReRoutes'
Expand Down Expand Up @@ -42,10 +43,10 @@ export const withTranslateRoutes = (userNextConfig: NextConfigWithNTR): NextConf
const pagesPath = getPagesPath(pagesDirectory)

const ntrData = createNtrData(userNextConfig, pagesPath)
setNtrData(ntrData)

const { routesTree, locales, defaultLocale } = ntrData

const { redirects, rewrites } = getRouteBranchReRoutes({ locales, routeBranch: routesTree, defaultLocale })
const { routesTree } = ntrData
const { redirects, rewrites } = getRouteBranchReRoutes({ routeBranch: routesTree })
const sortedRedirects = sortBySpecificity(redirects)
const sortedRewrites = sortBySpecificity(rewrites)

Expand Down
2 changes: 1 addition & 1 deletion src/react/enhanceNextRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const enhancePushReplace =
const enhancePrefetch =
<R extends NextRouter | SingletonRouter>(router: R) =>
(inputUrl: string, asPath?: string, options?: PrefetchOptions) => {
const locale = getLocale(router, options?.locale)
const locale = getLocale({ router, locale: options?.locale, url: inputUrl })
const parsedInputUrl = urlToFileUrl(inputUrl, locale)

if (getNtrData().debug === 'withPrefetch') {
Expand Down
5 changes: 3 additions & 2 deletions src/react/fileUrlToUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { normalizePathTrailingSlash } from 'next/dist/client/normalize-trailing-
import { parse as parsePathPattern, compile as compilePath } from 'path-to-regexp'
import { format as formatUrl, UrlObject } from 'url'

import { isDefaultLocale } from '../shared/isDefaultLocale'
import { ignoreSegmentPathRegex, optionalMatchAllFilepathPartRegex } from '../shared/regex'
import { ntrMessagePrefix } from '../shared/withNtrPrefix'
import type { TRouteBranch } from '../types'
Expand Down Expand Up @@ -117,7 +118,7 @@ export const fileUrlToUrl = (url: UrlObject | URL | string, locale: string, { th
try {
const { pathname, query, hash } = fileUrlToFileUrlObject(url)

const { routesTree, defaultLocale } = getNtrData()
const { routesTree, i18n } = getNtrData()

const pathParts = (pathname || '/')
.replace(/^\/|\/$/g, '')
Expand All @@ -134,7 +135,7 @@ export const fileUrlToUrl = (url: UrlObject | URL | string, locale: string, { th
}
}

return `${locale !== defaultLocale ? `/${locale}` : ''}${formatUrl({
return `${!isDefaultLocale(locale, i18n) ? `/${locale}` : ''}${formatUrl({
pathname: newPathname,
query,
hash,
Expand Down
30 changes: 26 additions & 4 deletions src/react/getLocale.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
import { NextRouter, SingletonRouter } from 'next/router'
import { UrlObject, parse as parseUrl } from 'url'

import { getNtrData } from './ntrData'

export const getLocale = (
{ locale, defaultLocale, locales }: NextRouter | SingletonRouter,
explicitLocale?: string | false,
) => explicitLocale || locale || defaultLocale || locales?.[0] || getNtrData().defaultLocale || getNtrData().locales[0]
export const getLocale = ({
router,
locale: explicitLocale,
url,
}: {
router?: NextRouter | SingletonRouter
locale?: string | false
url?: string | UrlObject | URL
} = {}) => {
if (explicitLocale) {
return explicitLocale
}
const { i18n } = getNtrData()

// explicitLocale === false if opted-out of automatically handling the locale prefixing
// Cf. https://nextjs.org/docs/advanced-features/i18n-routing#transition-between-locales
if (explicitLocale === false && url) {
const { pathname } = typeof url === 'string' ? parseUrl(url) : url
const localeSegment = pathname?.split('/')[1]
if (localeSegment && i18n.locales.includes(localeSegment)) {
return localeSegment
}
}
return router?.locale || router?.defaultLocale || i18n.defaultLocale || router?.locales?.[0] || i18n.locales[0]
}
74 changes: 57 additions & 17 deletions src/react/removeLangPrefix.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,67 @@
import { isDefaultLocale } from '../shared/isDefaultLocale'
import { getNtrData } from './ntrData'

export function removeLangPrefix(pathname: string, toArray?: false, locale?: string): string
export function removeLangPrefix(pathname: string, toArray: true, locale?: string): string[]
export function removeLangPrefix(pathname: string, toArray?: boolean, givenLocale?: string): string | string[] {
const pathParts = pathname.split('/').filter(Boolean)
const { routesTree, defaultLocale, locales } = getNtrData()
/**
* Remove both the lang prefix and the root prefix from a pathname
*
* (The root prefix is a prefix that can be added for a locale between the locale prefix
* and the rest of the pathname. It is defined in the root _routes.json file for the "/" key.)
*/
export function removeLangPrefix(
pathname: string,
/**
* If locale is explicitely given, removeLangPrefix will use it,
* it it is not, removeLangPrefix will try to deduce the locale from the pathname
*/
locale?: string,
): string {
const {
routesTree,
i18n,
i18n: { locales },
} = getNtrData()
let lang = locale
let root = ''

const getLangRoot = (lang: string) => routesTree.paths[lang] || routesTree.paths.default

const defaultLocaleRoot = defaultLocale && getLangRoot(defaultLocale)
const hasLangPrefix = givenLocale ? pathParts[0] === givenLocale : locales.includes(pathParts[0])
const hasDefaultLocalePrefix = !hasLangPrefix && !!defaultLocaleRoot && pathParts[0] === defaultLocaleRoot
const hasGivenLocalePrefix = givenLocale ? pathParts[hasLangPrefix ? 1 : 0] === getLangRoot(givenLocale) : false
if (locale) {
root = getLangRoot(locale)
} else {
const prefixLocale = locales.find((locale) => new RegExp(`\\/${locale}(\\/|$)`).test(pathname))
if (prefixLocale) {
lang = prefixLocale
root = getLangRoot(prefixLocale)
} else {
for (const l of locales) {
if (isDefaultLocale(l, i18n)) {
lang = l
root = getLangRoot(l)
break
}
}
}
}

let remainingPathname: string | undefined = undefined

if (!lang) {
return pathname
}

const fullPrefix = `/${lang}/${root}`

if (!hasLangPrefix && !hasDefaultLocalePrefix && !hasGivenLocalePrefix) {
return toArray ? pathParts : pathname
if (root && pathname.startsWith(fullPrefix)) {
remainingPathname = pathname.slice(fullPrefix.length)
} else if (root && pathname.startsWith(`/${root}`)) {
remainingPathname = pathname.slice(root.length + 1)
} else if (pathname.startsWith(`/${lang}`)) {
remainingPathname = pathname.slice(lang.length + 1)
}

const locale = givenLocale || hasLangPrefix ? pathParts[0] : defaultLocale
const localeRootParts = (locale || hasGivenLocalePrefix) && getLangRoot(locale)?.split('/')
const nbPathPartsToRemove =
(hasLangPrefix ? 1 : 0) +
(localeRootParts && (!hasLangPrefix || pathParts[1] === localeRootParts[0]) ? localeRootParts.length : 0)
if (typeof remainingPathname === 'string' && /^($|\/)/.test(remainingPathname)) {
return remainingPathname
}

return toArray ? pathParts.slice(nbPathPartsToRemove) : `/${pathParts.slice(nbPathPartsToRemove).join('/')}`
return pathname
}
Loading