Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add locale: false for custom-routes + i18n #19164

Merged
merged 4 commits into from
Nov 14, 2020
Merged
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
38 changes: 38 additions & 0 deletions docs/api-reference/next.config.js/headers.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,41 @@ module.exports = {
},
}
```

### Headers with i18n support

When leveraging [`i18n` support](/docs/advanced-features/i18n-routing.md) with headers each `source` is automatically prefixed to handle the configured `locales` unless you add `locale: false` to the header:

```js
module.exports = {
i18n: {
locales: ['en', 'fr', 'de'],
defaultLocale: 'en',
},

async headers() {
return [
{
source: '/with-locale', // automatically handles all locales
headers: [
{
key: 'x-hello',
value: 'world',
},
],
},
{
// does not handle locales automatically since locale: false is set
source: '/nl/with-locale-manual',
locale: false,
headers: [
{
key: 'x-hello',
value: 'world',
},
],
},
]
},
}
```
30 changes: 30 additions & 0 deletions docs/api-reference/next.config.js/redirects.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,34 @@ module.exports = {
}
```

### Redirects with i18n support

When leveraging [`i18n` support](/docs/advanced-features/i18n-routing.md) with redirects each `source` and `destination` is automatically prefixed to handle the configured `locales` unless you add `locale: false` to the redirect:

```js
module.exports = {
i18n: {
locales: ['en', 'fr', 'de'],
defaultLocale: 'en',
},

async redirects() {
return [
{
source: '/with-locale', // automatically handles all locales
destination: '/another', // automatically passes the locale on
permanent: false,
},
{
// does not handle locales automatically since locale: false is set
source: '/nl/with-locale-manual',
destination: '/nl/another',
locale: false,
permanent: false,
},
]
},
}
```

In some rare cases, you might need to assign a custom status code for older HTTP Clients to properly redirect. In these cases, you can use the `statusCode` property instead of the `permanent` property, but not both. Note: to ensure IE11 compatibility a `Refresh` header is automatically added for the 308 status code.
28 changes: 28 additions & 0 deletions docs/api-reference/next.config.js/rewrites.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,31 @@ module.exports = {
},
}
```

### Rewrites with i18n support

When leveraging [`i18n` support](/docs/advanced-features/i18n-routing.md) with rewrites each `source` and `destination` is automatically prefixed to handle the configured `locales` unless you add `locale: false` to the rewrite:

```js
module.exports = {
i18n: {
locales: ['en', 'fr', 'de'],
defaultLocale: 'en',
},

async rewrites() {
return [
{
source: '/with-locale', // automatically handles all locales
destination: '/another', // automatically passes the locale on
},
{
// does not handle locales automatically since locale: false is set
source: '/nl/with-locale-manual',
destination: '/nl/another',
locale: false,
},
]
},
}
```
15 changes: 14 additions & 1 deletion packages/next/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ export default async function build(
const buildCustomRoute = (
r: {
source: string
locale?: false
basePath?: false
statusCode?: number
destination?: string
Expand All @@ -262,14 +263,26 @@ export default async function build(
) => {
const keys: any[] = []

if (r.basePath !== false) {
if (r.basePath !== false && (!config.i18n || r.locale === false)) {
r.source = `${config.basePath}${r.source}`

if (r.destination && r.destination.startsWith('/')) {
r.destination = `${config.basePath}${r.destination}`
}
}

if (config.i18n && r.locale !== false) {
const basePath = r.basePath !== false ? config.basePath || '' : ''

r.source = `${basePath}/:nextInternalLocale(${config.i18n.locales
.map((locale: string) => escapeStringRegexp(locale))
.join('|')})${r.source}`

if (r.destination && r.destination?.startsWith('/')) {
r.destination = `${basePath}/:nextInternalLocale${r.destination}`
}
}

const routeRegex = pathToRegexp(r.source, keys, {
strict: true,
sensitive: false,
Expand Down
55 changes: 49 additions & 6 deletions packages/next/build/webpack/loaders/next-serverless-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,22 +191,51 @@ const nextServerlessLoader: loader.Loader = function () {
rewrite.destination,
params,
parsedUrl.query,
true,
"${basePath}"
true
)

Object.assign(parsedUrl.query, parsedDestination.query)
delete parsedDestination.query

Object.assign(parsedUrl, parsedDestination)

if (parsedUrl.pathname === '${page}'){
let fsPathname = parsedUrl.pathname

${
basePath
? `
fsPathname = fsPathname.replace(
new RegExp('^${basePath}'),
''
) || '/'
`
: ''
}

${
i18n
? `
const destLocalePathResult = normalizeLocalePath(
fsPathname,
i18n.locales
)
fsPathname = destLocalePathResult.pathname

parsedUrl.query.nextInternalLocale = (
destLocalePathResult.detectedLocale ||
params.nextInternalLocale
)
`
: ''
}

if (fsPathname === '${page}'){
break
}
${
pageIsDynamicRoute
? `
const dynamicParams = dynamicRouteMatcher(parsedUrl.pathname);\
const dynamicParams = dynamicRouteMatcher(fsPathname);\
if (dynamicParams) {
parsedUrl.query = {
...parsedUrl.query,
Expand Down Expand Up @@ -235,12 +264,10 @@ const nextServerlessLoader: loader.Loader = function () {
const handleLocale = i18nEnabled
? `
// get pathname from URL with basePath stripped for locale detection
const i18n = ${i18n}
const accept = require('@hapi/accept')
const cookie = require('next/dist/compiled/cookie')
const { detectLocaleCookie } = require('next/dist/next-server/lib/i18n/detect-locale-cookie')
const { detectDomainLocale } = require('next/dist/next-server/lib/i18n/detect-domain-locale')
const { normalizeLocalePath } = require('next/dist/next-server/lib/i18n/normalize-locale-path')
let locales = i18n.locales
let defaultLocale = i18n.defaultLocale
let detectedLocale = detectLocaleCookie(req, i18n.locales)
Expand Down Expand Up @@ -400,6 +427,9 @@ const nextServerlessLoader: loader.Loader = function () {
${dynamicRouteImports}
const { parse: parseUrl } = require('url')
const { apiResolver } = require('next/dist/next-server/server/api-utils')
const { normalizeLocalePath } = require('next/dist/next-server/lib/i18n/normalize-locale-path')
const i18n = ${i18n || '{}'}

${rewriteImports}

${dynamicRouteMatcher}
Expand All @@ -416,6 +446,12 @@ const nextServerlessLoader: loader.Loader = function () {
// to ensure we are using the correct values
const trustQuery = req.headers['${vercelHeader}']
const parsedUrl = handleRewrites(parseUrl(req.url, true))

if (parsedUrl.query.nextInternalLocale) {
detectedLocale = parsedUrl.query.nextInternalLocale
delete parsedUrl.query.nextInternalLocale
}

let hasValidParams = true

${normalizeDynamicRouteParams}
Expand Down Expand Up @@ -482,6 +518,8 @@ const nextServerlessLoader: loader.Loader = function () {
const {PERMANENT_REDIRECT_STATUS} = require('next/dist/next-server/lib/constants')
const buildManifest = require('${buildManifest}');
const reactLoadableManifest = require('${reactLoadableManifest}');
const { normalizeLocalePath } = require('next/dist/next-server/lib/i18n/normalize-locale-path')
const i18n = ${i18n || '{}'}

const appMod = require('${absoluteAppPath}')
let App = appMod.default || appMod.then && appMod.then(mod => mod.default);
Expand Down Expand Up @@ -606,6 +644,11 @@ const nextServerlessLoader: loader.Loader = function () {

${handleLocale}

if (parsedUrl.query.nextInternalLocale) {
detectedLocale = parsedUrl.query.nextInternalLocale
delete parsedUrl.query.nextInternalLocale
}

const renderOpts = Object.assign(
{
Component,
Expand Down
16 changes: 13 additions & 3 deletions packages/next/lib/load-custom-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ export type Rewrite = {
source: string
destination: string
basePath?: false
locale?: false
}

export type Header = {
source: string
basePath?: false
locale?: false
headers: Array<{ key: string; value: string }>
}

// internal type used for validation (not user facing)
export type Redirect = Rewrite & {
statusCode?: number
permanent?: boolean
destination: string
basePath?: false
}

export const allowedStatusCodes = new Set([301, 302, 303, 307, 308])
Expand Down Expand Up @@ -157,10 +157,11 @@ function checkCustomRoutes(
'source',
'destination',
'basePath',
'locale',
...(isRedirect ? ['statusCode', 'permanent'] : []),
])
} else {
allowedKeys = new Set(['source', 'headers', 'basePath'])
allowedKeys = new Set(['source', 'headers', 'basePath', 'locale'])
}

for (const route of routes) {
Expand Down Expand Up @@ -201,6 +202,10 @@ function checkCustomRoutes(
invalidParts.push('`basePath` must be undefined or false')
}

if (typeof route.locale !== 'undefined' && route.locale !== false) {
invalidParts.push('`locale` must be undefined or true')
}

if (!route.source) {
invalidParts.push('`source` is missing')
} else if (typeof route.source !== 'string') {
Expand Down Expand Up @@ -386,11 +391,13 @@ export default async function loadCustomRoutes(
source: '/:file((?:[^/]+/)*[^/]+\\.\\w+)/',
destination: '/:file',
permanent: true,
locale: config.i18n ? false : undefined,
},
{
source: '/:notfile((?:[^/]+/)*[^/\\.]+)',
destination: '/:notfile/',
permanent: true,
locale: config.i18n ? false : undefined,
}
)
if (config.basePath) {
Expand All @@ -399,20 +406,23 @@ export default async function loadCustomRoutes(
destination: config.basePath + '/',
permanent: true,
basePath: false,
locale: config.i18n ? false : undefined,
})
}
} else {
redirects.unshift({
source: '/:path+/',
destination: '/:path+',
permanent: true,
locale: config.i18n ? false : undefined,
})
if (config.basePath) {
redirects.unshift({
source: config.basePath + '/',
destination: config.basePath,
permanent: true,
basePath: false,
locale: config.i18n ? false : undefined,
})
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export default function prepareDestination(

// clone query so we don't modify the original
query = Object.assign({}, query)
const hadLocale = query.__nextLocale
delete query.__nextLocale
delete query.__nextDefaultLocale

Expand Down Expand Up @@ -121,7 +122,12 @@ export default function prepareDestination(

// add path params to query if it's not a redirect and not
// already defined in destination query or path
const paramKeys = Object.keys(params)
let paramKeys = Object.keys(params)

// remove internal param for i18n
if (hadLocale) {
paramKeys = paramKeys.filter((name) => name !== 'nextInternalLocale')
}

if (
appendParamsToQuery &&
Expand All @@ -144,7 +150,7 @@ export default function prepareDestination(
const [pathname, hash] = newUrl.split('#')
parsedDestination.pathname = pathname
parsedDestination.hash = `${hash ? '#' : ''}${hash || ''}`
delete parsedDestination.search
delete (parsedDestination as any).search
} catch (err) {
if (err.message.match(/Expected .*? to not repeat, but got an array/)) {
throw new Error(
Expand Down
Loading