diff --git a/packages/now-next/package.json b/packages/now-next/package.json index e4efadcdbb2..caebb0a88d0 100644 --- a/packages/now-next/package.json +++ b/packages/now-next/package.json @@ -29,7 +29,7 @@ "@vercel/nft": "0.9.2", "async-sema": "3.0.1", "buffer-crc32": "0.2.13", - "escape-string-regexp": "3.0.0", + "escape-string-regexp": "2.0.0", "execa": "2.0.4", "find-up": "4.1.0", "fs-extra": "7.0.0", diff --git a/packages/now-next/src/index.ts b/packages/now-next/src/index.ts index 1ae4f020e46..efb1e93e626 100644 --- a/packages/now-next/src/index.ts +++ b/packages/now-next/src/index.ts @@ -19,6 +19,7 @@ import { import { nodeFileTrace, NodeFileTraceReasons } from '@vercel/nft'; import { Sema } from 'async-sema'; import { ChildProcess, fork } from 'child_process'; +// escape-string-regexp version must match Next.js version import escapeStringRegexp from 'escape-string-regexp'; import findUp from 'find-up'; import { lstat, pathExists, readFile, remove, writeFile } from 'fs-extra'; @@ -31,6 +32,7 @@ import buildUtils from './build-utils'; import createServerlessConfig from './create-serverless-config'; import nextLegacyVersions from './legacy-versions'; import { + addLocaleOrDefault, createLambdaFromPseudoLayers, createPseudoLayer, EnvConfig, @@ -47,6 +49,7 @@ import { getRoutesManifest, getSourceFilePathFromPage, isDynamicRoute, + normalizeLocalePath, normalizePackageJson, normalizePage, PseudoLayer, @@ -222,6 +225,10 @@ export const build = async ({ routes: Route[]; images?: { domains: string[]; sizes: number[] }; output: Files; + wildcard?: Array<{ + domain: string; + value: string; + }>; watch?: string[]; childProcesses: ChildProcess[]; }> => { @@ -370,7 +377,7 @@ export const build = async ({ } console.log('Installing dependencies...'); - await runNpmInstall(entryPath, ['--prefer-offline'], spawnOpts, meta); + await runNpmInstall(entryPath, [], spawnOpts, meta); // Refetch Next version now that dependencies are installed. // This will now resolve the actual installed Next version, @@ -432,6 +439,24 @@ export const build = async ({ let dynamicRoutes: Route[] = []; // whether they have enabled pages/404.js as the custom 404 page let hasPages404 = false; + let buildId = ''; + let escapedBuildId = ''; + + if (isLegacy || isSharedLambdas) { + try { + buildId = await readFile( + path.join(entryPath, outputDirectory, 'BUILD_ID'), + 'utf8' + ); + escapedBuildId = escapeStringRegexp(buildId); + } catch (err) { + throw new NowBuildError({ + code: 'NOW_NEXT_NO_BUILD_ID', + message: + 'The BUILD_ID file was not found in the Output Directory. Did you forget to run "next build" in your Build Command?', + }); + } + } if (routesManifest) { switch (routesManifest.version) { @@ -462,7 +487,7 @@ export const build = async ({ continue; } - dataRoutes.push({ + const route = { src: ( dataRoute.namedDataRouteRegex || dataRoute.dataRouteRegex ).replace(/^\^/, `^${appMountPrefixNoTrailingSlash}`), @@ -481,7 +506,31 @@ export const build = async ({ }` ), check: true, - }); + }; + + const { i18n } = routesManifest; + + if (i18n) { + route.src = route.src.replace( + // we need to double escape the build ID here + // to replace it properly + `/${escapedBuildId}/`, + `/${escapedBuildId}/(?${ + ssgDataRoute ? '' : ':' + }${i18n.locales + .map(locale => escapeStringRegexp(locale)) + .join('|')})/` + ); + + // make sure to route to the correct prerender output + if (ssgDataRoute) { + route.dest = route.dest.replace( + `/${buildId}/`, + `/${buildId}/$nextLocale/` + ); + } + } + dataRoutes.push(route); } } @@ -693,12 +742,7 @@ export const build = async ({ if (isLegacy) { debug('Running npm install --production...'); - await runNpmInstall( - entryPath, - ['--prefer-offline', '--production'], - spawnOpts, - meta - ); + await runNpmInstall(entryPath, ['--production'], spawnOpts, meta); } if (process.env.NPM_AUTH_TOKEN) { @@ -715,27 +759,7 @@ export const build = async ({ const staticPages: { [key: string]: FileFsRef } = {}; const dynamicPages: string[] = []; let static404Page: string | undefined; - let buildId = ''; let page404Path = ''; - let escapedBuildId = ''; - - if (isLegacy || isSharedLambdas) { - try { - buildId = await readFile( - path.join(entryPath, outputDirectory, 'BUILD_ID'), - 'utf8' - ); - escapedBuildId = escapeStringRegexp(buildId); - } catch (err) { - console.error( - 'BUILD_ID not found in ".next". The "package.json" "build" script did not run "next build"' - ); - throw new NowBuildError({ - code: 'NOW_NEXT_NO_BUILD_ID', - message: 'Missing BUILD_ID', - }); - } - } if (isLegacy) { const filesAfterBuild = await glob('**', entryPath); @@ -844,7 +868,10 @@ export const build = async ({ Object.keys(staticPageFiles).forEach((page: string) => { const pathname = page.replace(/\.html$/, ''); - const routeName = normalizePage(pathname); + const routeName = normalizeLocalePath( + normalizePage(pathname), + routesManifest?.i18n?.locales + ).pathname; // Prerendered routes emit a `.html` file but should not be treated as a // static page. @@ -877,6 +904,17 @@ export const build = async ({ ? path.join(entryDirectory, '_errors/404') : undefined; + // TODO: locale specific 404s + const { i18n } = routesManifest || {}; + + if (!static404Page && i18n) { + static404Page = staticPages[ + path.join(entryDirectory, i18n.defaultLocale, '404') + ] + ? path.join(entryDirectory, i18n.defaultLocale, '404') + : undefined; + } + // > 1 because _error is a lambda but isn't used if a static 404 is available const pageKeys = Object.keys(pages); let hasLambdas = !static404Page || pageKeys.length > 1; @@ -1189,25 +1227,36 @@ export const build = async ({ }; } - const pageLambdaRoute: Route = { - src: `^${escapeStringRegexp(outputName).replace( - /\/index$/, - '(/|/index|)' - )}/?$`, - dest: `${path.join('/', currentLambdaGroup.lambdaIdentifier)}`, - headers: { - 'x-nextjs-page': outputName, - }, - check: true, + const addPageLambdaRoute = (escapedOutputPath: string) => { + const pageLambdaRoute: Route = { + src: `^${escapedOutputPath.replace(/\/index$/, '(/|/index|)')}/?$`, + dest: `${path.join('/', currentLambdaGroup.lambdaIdentifier)}`, + headers: { + 'x-nextjs-page': outputName, + }, + check: true, + }; + + // we only need to add the additional routes if shared lambdas + // is enabled + if (routeIsDynamic) { + dynamicPageLambdaRoutes.push(pageLambdaRoute); + dynamicPageLambdaRoutesMap[outputName] = pageLambdaRoute; + } else { + pageLambdaRoutes.push(pageLambdaRoute); + } }; - // we only need to add the additional routes if shared lambdas - // is enabled - if (routeIsDynamic) { - dynamicPageLambdaRoutes.push(pageLambdaRoute); - dynamicPageLambdaRoutesMap[outputName] = pageLambdaRoute; + const { i18n } = routesManifest || {}; + + if (i18n) { + addPageLambdaRoute( + `[/]?(?:${i18n.locales + .map(locale => escapeStringRegexp(locale)) + .join('|')})?${escapeStringRegexp(outputName)}` + ); } else { - pageLambdaRoutes.push(pageLambdaRoute); + addPageLambdaRoute(escapeStringRegexp(outputName)); } if (page === '_error.js' || (hasPages404 && page === '404.js')) { @@ -1336,7 +1385,32 @@ export const build = async ({ new Set(prerenderManifest.omittedRoutes) ).then(arr => arr.map(route => { - route.src = route.src.replace('^', `^${dynamicPrefix}`); + const { i18n } = routesManifest || {}; + + if (i18n) { + const { pathname } = url.parse(route.dest!); + const isFallback = prerenderManifest.fallbackRoutes[pathname!]; + + route.src = route.src.replace( + '^', + `^${dynamicPrefix ? `${dynamicPrefix}[/]?` : '[/]?'}(?${ + isFallback ? '' : ':' + }${i18n.locales + .map(locale => escapeStringRegexp(locale)) + .join('|')})?` + ); + + if (isFallback) { + // ensure destination has locale prefix to match prerender output + // path so that the prerender object is used + route.dest = route.dest!.replace( + `${path.join('/', entryDirectory, '/')}`, + `${path.join('/', entryDirectory, '$nextLocale', '/')}` + ); + } + } else { + route.src = route.src.replace('^', `^${dynamicPrefix}`); + } return route; }) ); @@ -1369,6 +1443,31 @@ export const build = async ({ /\/\/ __LAUNCHER_PAGE_HANDLER__/g, ` const url = require('url'); + + ${ + routesManifest?.i18n + ? ` + function stripLocalePath(pathname) { + // first item will be empty string from splitting at first char + const pathnameParts = pathname.split('/') + + ;(${JSON.stringify( + routesManifest.i18n.locales + )}).some((locale) => { + if (pathnameParts[1].toLowerCase() === locale.toLowerCase()) { + pathnameParts.splice(1, 1) + pathname = pathnameParts.join('/') || '/' + return true + } + return false + }) + + return pathname + } + ` + : `function stripLocalePath(pathname) { return pathname }` + } + page = function(req, res) { try { const pages = { @@ -1393,7 +1492,7 @@ export const build = async ({ if (!toRender) { try { const { pathname } = url.parse(req.url) - toRender = pathname.replace(/\\/$/, '') + toRender = stripLocalePath(pathname).replace(/\\/$/, '') } catch (_) { // handle failing to parse url res.statusCode = 400 @@ -1412,6 +1511,7 @@ export const build = async ({ .replace(new RegExp('/_next/data/${escapedBuildId}/'), '/') .replace(/\\.json$/, '') + toRender = stripLocalePath(toRender) currentPage = pages[toRender] } @@ -1522,7 +1622,15 @@ export const build = async ({ let prerenderGroup = 1; const onPrerenderRoute = ( routeKey: string, - { isBlocking, isFallback }: { isBlocking: boolean; isFallback: boolean } + { + isBlocking, + isFallback, + locale, + }: { + isBlocking: boolean; + isFallback: boolean; + locale?: string; + } ) => { if (isBlocking && isFallback) { throw new NowBuildError({ @@ -1532,7 +1640,22 @@ export const build = async ({ } // Get the route file as it'd be mounted in the builder output - const routeFileNoExt = routeKey === '/' ? '/index' : routeKey; + let routeFileNoExt = routeKey === '/' ? '/index' : routeKey; + const origRouteFileNoExt = routeFileNoExt; + + const nonDynamicSsg = + !isFallback && + !isBlocking && + !prerenderManifest.staticRoutes[routeKey].srcRoute; + + // if there isn't a srcRoute then it's a non-dynamic SSG page and + if (nonDynamicSsg || isFallback) { + routeFileNoExt = addLocaleOrDefault( + routeFileNoExt, + routesManifest, + locale + ); + } const htmlFsRef = isBlocking ? // Blocking pages do not have an HTML fallback @@ -1542,7 +1665,11 @@ export const build = async ({ pagesDir, isFallback ? // Fallback pages have a special file. - prerenderManifest.fallbackRoutes[routeKey].fallback + addLocaleOrDefault( + prerenderManifest.fallbackRoutes[routeKey].fallback, + routesManifest, + locale + ) : // Otherwise, the route itself should exist as a static HTML // file. `${routeFileNoExt}.html` @@ -1581,14 +1708,25 @@ export const build = async ({ } const outputPathPage = path.posix.join(entryDirectory, routeFileNoExt); + const outputPathPageOrig = path.posix.join( + entryDirectory, + origRouteFileNoExt + ); let lambda: undefined | Lambda; - const outputPathData = path.posix.join(entryDirectory, dataRoute); + let outputPathData = path.posix.join(entryDirectory, dataRoute); + + if (nonDynamicSsg || isFallback) { + outputPathData = outputPathData.replace( + new RegExp(`${escapeStringRegexp(origRouteFileNoExt)}.json$`), + `${routeFileNoExt}.json` + ); + } if (isSharedLambdas) { const outputSrcPathPage = path.join( '/', srcRoute == null - ? outputPathPage + ? outputPathPageOrig : path.join(entryDirectory, srcRoute === '/' ? '/index' : srcRoute) ); @@ -1597,7 +1735,7 @@ export const build = async ({ } else { const outputSrcPathPage = srcRoute == null - ? outputPathPage + ? outputPathPageOrig : path.posix.join( entryDirectory, srcRoute === '/' ? '/index' : srcRoute @@ -1646,6 +1784,18 @@ export const build = async ({ ++prerenderGroup; } + + if ((nonDynamicSsg || isFallback) && routesManifest?.i18n && !locale) { + // load each locale + for (const locale of routesManifest.i18n.locales) { + if (locale === routesManifest.i18n.defaultLocale) continue; + onPrerenderRoute(routeKey, { + isBlocking, + isFallback, + locale, + }); + } + } }; Object.keys(prerenderManifest.staticRoutes).forEach(route => @@ -1774,6 +1924,8 @@ export const build = async ({ } } + const { i18n } = routesManifest || {}; + return { output: { ...publicDirectoryFiles, @@ -1784,6 +1936,17 @@ export const build = async ({ ...staticFiles, ...staticDirectoryFiles, }, + wildcard: i18n?.domains + ? i18n?.domains.map(item => { + return { + domain: item.domain, + value: + item.defaultLocale === i18n.defaultLocale + ? '' + : `/${item.defaultLocale}`, + }; + }) + : undefined, images: imagesManifest?.images ? { domains: imagesManifest.images.domains, @@ -1805,14 +1968,130 @@ export const build = async ({ ...headers, // redirects - ...redirects, + ...redirects.map(redir => { + if (i18n) { + // detect the trailing slash redirect and make sure it's + // kept above the wildcard mapping to prevent erroneous redirects + // since non-continue routes come after continue the $wildcard + // route will come before the redirect otherwise and if the + // redirect is triggered it breaks locale mapping + if ( + redir.status === 308 && + (redir.dest === '/$1' || redir.dest === '/$1/') + ) { + // we set continue true + (redir as any).continue = true; + } + } + return redir; + }), + + ...(i18n + ? [ + // Handle auto-adding current default locale to path based on $wildcard + { + src: `^${path.join( + '/', + entryDirectory, + '/' + )}(?!(?:_next/.*|${i18n.locales + .map(locale => escapeStringRegexp(locale)) + .join('|')})(?:/.*|$))(.*)$`, + // TODO: this needs to contain or not contain a trailing slash + // to prevent the trailing slash redirect from being triggered + dest: '$wildcard/$1', + continue: true, + }, + + // Handle redirecting to locale specific domains + ...(i18n.domains + ? [ + { + // TODO: enable redirecting between domains, will require + // updating the src with the desired locales to redirect + src: '/', + locale: { + redirect: i18n.domains.reduce( + (prev: Record, item) => { + prev[item.defaultLocale] = `http${ + item.http ? '' : 's' + }://${item.domain}/`; + return prev; + }, + {} + ), + cookie: 'NEXT_LOCALE', + }, + continue: true, + }, + ] + : []), + + // Handle redirecting to locale paths + { + // TODO: enable redirecting between paths, will require + // updating the src with the desired locales to redirect. + // if default locale is included in this src it won't be visitable + // by users who prefer another language since the cookie isn't set + // on redirect currently like in `next start` + src: '/', + locale: { + redirect: i18n.locales.reduce( + (prev: Record, locale) => { + prev[locale] = + locale === i18n.defaultLocale ? `/` : `/${locale}`; + return prev; + }, + {} + ), + cookie: 'NEXT_LOCALE', + }, + continue: true, + }, + + { + src: `^${path.join('/', entryDirectory)}$`, + dest: `/${i18n.defaultLocale}`, + continue: true, + }, + + // Auto-prefix non-locale path with default locale + { + src: `^${path.join( + '/', + entryDirectory, + '/' + )}(?!(?:_next/.*|${i18n.locales + .map(locale => escapeStringRegexp(locale)) + .join('|')})(?:/.*|$))(.*)$`, + dest: `/${i18n.defaultLocale}/$1`, + continue: true, + }, + ] + : []), // Make sure to 404 for the /404 path itself - { - src: path.join('/', entryDirectory, '404'), - status: 404, - continue: true, - }, + ...(i18n + ? [ + { + src: `${path.join( + '/', + entryDirectory, + '/' + )}(?:${i18n.locales + .map(locale => escapeStringRegexp(locale)) + .join('|')})?[/]?404`, + status: 404, + continue: true, + }, + ] + : [ + { + src: path.join('/', entryDirectory, '404'), + status: 404, + continue: true, + }, + ]), // Next.js page lambdas, `static/` folder, reserved assets, and `public/` // folder @@ -1842,6 +2121,40 @@ export const build = async ({ dest: '$0', }, + // remove default locale prefix to check public files + ...(i18n + ? [ + { + src: `${path.join( + '/', + entryDirectory, + i18n.defaultLocale, + '/' + )}(.*)`, + dest: `${path.join('/', entryDirectory, '/')}$1`, + check: true, + }, + ] + : []), + + // for non-shared lambdas remove locale prefix if present + // to allow checking for lambda + ...(isSharedLambdas || !i18n + ? [] + : [ + { + src: `${path.join( + '/', + entryDirectory, + '/' + )}(?:${i18n?.locales + .map(locale => escapeStringRegexp(locale)) + .join('|')})/(.*)`, + dest: '/$1', + check: true, + }, + ]), + // routes that are called after each rewrite or after routes // if there no rewrites { handle: 'rewrite' }, @@ -1882,39 +2195,60 @@ export const build = async ({ // Custom Next.js 404 page { handle: 'error' } as Handler, - isSharedLambdas - ? { - src: path.join('/', entryDirectory, '.*'), - // if static 404 is not present but we have pages/404.js - // it is a lambda due to _app getInitialProps - dest: path.join( - '/', - (static404Page - ? static404Page - : pageLambdaMap[page404Path]) as string - ), - - status: 404, - headers: { - 'x-nextjs-page': page404Path, + ...(i18n && static404Page + ? [ + { + src: `${path.join( + '/', + entryDirectory, + '/' + )}(?${i18n.locales + .map(locale => escapeStringRegexp(locale)) + .join('|')})(/.*|$)`, + dest: '/$nextLocale/404', + status: 404, }, - } - : { - src: path.join('/', entryDirectory, '.*'), - // if static 404 is not present but we have pages/404.js - // it is a lambda due to _app getInitialProps - dest: static404Page - ? path.join('/', static404Page) - : path.join( - '/', - entryDirectory, - hasPages404 && - lambdas[path.join('./', entryDirectory, '404')] - ? '404' - : '_error' - ), - status: 404, - }, + { + src: path.join('/', entryDirectory, '.*'), + dest: `/${i18n.defaultLocale}/404`, + status: 404, + }, + ] + : [ + isSharedLambdas + ? { + src: path.join('/', entryDirectory, '.*'), + // if static 404 is not present but we have pages/404.js + // it is a lambda due to _app getInitialProps + dest: path.join( + '/', + (static404Page + ? static404Page + : pageLambdaMap[page404Path]) as string + ), + + status: 404, + headers: { + 'x-nextjs-page': page404Path, + }, + } + : { + src: path.join('/', entryDirectory, '.*'), + // if static 404 is not present but we have pages/404.js + // it is a lambda due to _app getInitialProps + dest: static404Page + ? path.join('/', static404Page) + : path.join( + '/', + entryDirectory, + hasPages404 && + lambdas[path.join('./', entryDirectory, '404')] + ? '404' + : '_error' + ), + status: 404, + }, + ]), ]), ], watch: [], diff --git a/packages/now-next/src/utils.ts b/packages/now-next/src/utils.ts index 85abf23b991..b68f37a3971 100644 --- a/packages/now-next/src/utils.ts +++ b/packages/now-next/src/utils.ts @@ -326,6 +326,15 @@ export type RoutesManifest = { namedDataRouteRegex?: string; routeKeys?: { [named: string]: string }; }>; + i18n?: { + defaultLocale: string; + locales: string[]; + domains?: Array<{ + http?: boolean; + domain: string; + defaultLocale: string; + }>; + }; }; export async function getRoutesManifest( @@ -1066,6 +1075,44 @@ function isDirectory(path: string) { return fs.existsSync(path) && fs.lstatSync(path).isDirectory(); } +export function normalizeLocalePath( + pathname: string, + locales?: string[] +): { + detectedLocale?: string; + pathname: string; +} { + let detectedLocale: string | undefined; + // first item will be empty string from splitting at first char + const pathnameParts = pathname.split('/'); + + (locales || []).some(locale => { + if (pathnameParts[1].toLowerCase() === locale.toLowerCase()) { + detectedLocale = locale; + pathnameParts.splice(1, 1); + pathname = pathnameParts.join('/') || '/'; + return true; + } + return false; + }); + + return { + pathname, + detectedLocale, + }; +} + +export function addLocaleOrDefault( + pathname: string, + routesManifest?: RoutesManifest, + locale?: string +) { + if (!routesManifest?.i18n) return pathname; + if (!locale) locale = routesManifest.i18n.defaultLocale; + + return locale ? `/${locale}${pathname}` : pathname; +} + export { excludeFiles, validateEntrypoint, diff --git a/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/next.config.js b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/next.config.js new file mode 100644 index 00000000000..4f05fa39ab8 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/next.config.js @@ -0,0 +1,20 @@ +module.exports = { + experimental: { + i18n: { + locales: ['nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en-US', 'en'], + defaultLocale: 'en-US', + // TODO: testing locale domains support, will require custom + // testing set-up as test accounts are used currently + domains: [ + { + domain: 'example.be', + defaultLocale: 'nl-BE', + }, + { + domain: 'example.fr', + defaultLocale: 'fr', + }, + ], + }, + }, +}; diff --git a/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/now.json b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/now.json new file mode 100644 index 00000000000..e99748b3cb2 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/now.json @@ -0,0 +1,318 @@ +{ + "version": 2, + "builds": [ + { + "src": "package.json", + "use": "@vercel/next", + "config": { + "sharedLambdas": false + } + } + ], + "probes": [ + { + "path": "/", + "headers": { + "accept-language": "en;q=0.9" + }, + "fetchOptions": { + "redirect": "manual" + }, + "status": 307, + "responseHeaders": { + "location": "//en/" + } + }, + { + "path": "/", + "headers": { + "accept-language": "nl;q=0.9" + }, + "fetchOptions": { + "redirect": "manual" + }, + "status": 307, + "responseHeaders": { + "location": "//nl/" + } + }, + { + "path": "/", + "headers": { + "accept-language": "nl-NL;q=0.9" + }, + "fetchOptions": { + "redirect": "manual" + }, + "status": 307, + "responseHeaders": { + "location": "//nl-NL/" + } + }, + { + "path": "/", + "headers": { + "accept-language": "fr;q=0.9" + }, + "fetchOptions": { + "redirect": "manual" + }, + "status": 307, + "responseHeaders": { + "location": "//fr/" + } + }, + { + "path": "/", + "headers": { + "accept-language": "en-US;q=0.9" + }, + "fetchOptions": { + "redirect": "manual" + }, + "status": 200, + "mustContain": "index page" + }, + { + "path": "/en-US", + "headers": { + "accept-language": "nl;q=0.9" + }, + "fetchOptions": { + "redirect": "manual" + }, + "status": 200, + "mustContain": "index page" + }, + + { + "path": "/", + "status": 200, + "mustContain": "index page" + }, + { + "path": "/", + "status": 200, + "mustContain": ">en-US<" + }, + { + "path": "/en", + "status": 200, + "mustContain": "index page" + }, + { + "path": "/en", + "status": 200, + "mustContain": ">en<" + }, + { + "path": "/fr", + "status": 200, + "mustContain": "index page" + }, + { + "path": "/fr", + "status": 200, + "mustContain": ">fr<" + }, + { + "path": "/nl", + "status": 200, + "mustContain": "index page" + }, + { + "path": "/nl", + "status": 200, + "mustContain": ">nl<" + }, + { + "path": "/nl-NL", + "status": 200, + "mustContain": "index page" + }, + { + "path": "/nl-NL", + "status": 200, + "mustContain": ">nl-NL<" + }, + + { + "path": "/non-existent", + "status": 404 + }, + { + "path": "/fr/non-existent", + "status": 404, + "mustContain": "lang=\"fr\"" + }, + { + "path": "/en/non-existent", + "status": 404, + "mustContain": "lang=\"en\"" + }, + { + "path": "/en-US/non-existent", + "status": 404, + "mustContain": "lang=\"en-US\"" + }, + { + "path": "/nl/non-existent", + "status": 404, + "mustContain": "lang=\"nl\"" + }, + { + "path": "/nl-NL/non-existent", + "status": 404, + "mustContain": "lang=\"nl-NL\"" + }, + + { + "path": "/hello.txt", + "status": 200, + "mustContain": "hello world!" + }, + + { + "path": "/gsp", + "status": 200, + "mustContain": "gsp page" + }, + { + "path": "/gsp", + "status": 200, + "mustContain": ">en-US<" + }, + { + "path": "/en/gsp", + "status": 200, + "mustContain": "gsp page" + }, + { + "path": "/en/gsp", + "status": 200, + "mustContain": ">en<" + }, + { + "path": "/nl/gsp", + "status": 200, + "mustContain": "gsp page" + }, + { + "path": "/nl/gsp", + "status": 200, + "mustContain": ">nl<" + }, + { + "path": "/fr/gsp", + "status": 200, + "mustContain": "gsp page" + }, + { + "path": "/fr/gsp", + "status": 200, + "mustContain": ">fr<" + }, + + { + "path": "/gssp", + "status": 200, + "mustContain": "gssp page" + }, + { + "path": "/gssp", + "status": 200, + "mustContain": ">en-US<" + }, + { + "path": "/en/gssp", + "status": 200, + "mustContain": "gssp page" + }, + { + "path": "/en/gssp", + "status": 200, + "mustContain": ">en<" + }, + { + "path": "/nl/gssp", + "status": 200, + "mustContain": "gssp page" + }, + { + "path": "/nl/gssp", + "status": 200, + "mustContain": ">nl<" + }, + { + "path": "/fr/gssp", + "status": 200, + "mustContain": "gssp page" + }, + { + "path": "/fr/gssp", + "status": 200, + "mustContain": ">fr<" + }, + + { + "path": "/gssp/first", + "status": 200, + "mustContain": "gssp page" + }, + { + "path": "/gssp/first", + "status": 200, + "mustContain": ">en-US<" + }, + { + "path": "/gssp/first", + "status": 200, + "mustContain": "slug\":\"first\"" + }, + { + "path": "/en/gssp/first", + "status": 200, + "mustContain": "gssp page" + }, + { + "path": "/en/gssp/first", + "status": 200, + "mustContain": ">en<" + }, + { + "path": "/en/gssp/first", + "status": 200, + "mustContain": "slug\":\"first\"" + }, + { + "path": "/nl/gssp/first", + "status": 200, + "mustContain": "gssp page" + }, + { + "path": "/nl/gssp/first", + "status": 200, + "mustContain": ">nl<" + }, + { + "path": "/nl/gssp/first", + "status": 200, + "mustContain": "slug\":\"first\"" + }, + { + "path": "/fr/gssp/first", + "status": 200, + "mustContain": "gssp page" + }, + { + "path": "/fr/gssp/first", + "status": 200, + "mustContain": ">fr<" + }, + { + "path": "/fr/gssp/first", + "status": 200, + "mustContain": "slug\":\"first\"" + } + ] +} diff --git a/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/package.json b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/package.json new file mode 100644 index 00000000000..80dcb99fc31 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "next": "canary", + "react": "^16.8.6", + "react-dom": "^16.8.6" + } +} diff --git a/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/another.js b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/another.js new file mode 100644 index 00000000000..23d80fdad94 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/another.js @@ -0,0 +1,31 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + + return ( + <> +

another page

+

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + +
+ + ); +} + +export const getServerSideProps = ({ locale, locales }) => { + return { + props: { + locale, + locales, + }, + }; +}; diff --git a/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/auto-export/index.js b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/auto-export/index.js new file mode 100644 index 00000000000..bcb719b2bdd --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/auto-export/index.js @@ -0,0 +1,21 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + + return ( + <> +

auto-export page

+

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + + + ); +} diff --git a/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/gsp/fallback/[slug].js b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/gsp/fallback/[slug].js new file mode 100644 index 00000000000..387aad4d153 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/gsp/fallback/[slug].js @@ -0,0 +1,44 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + + if (router.isFallback) return 'Loading...'; + + return ( + <> +

gsp page

+

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + +
+ + ); +} + +export const getStaticProps = ({ params, locale, locales }) => { + return { + props: { + params, + locale, + locales, + }, + }; +}; + +export const getStaticPaths = () => { + return { + // the default locale will be used since one isn't defined here + paths: ['first', 'second'].map(slug => ({ + params: { slug }, + })), + fallback: true, + }; +}; diff --git a/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/gsp/index.js b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/gsp/index.js new file mode 100644 index 00000000000..7b62978db77 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/gsp/index.js @@ -0,0 +1,32 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + + return ( + <> +

gsp page

+

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + +
+ + ); +} + +// TODO: should non-dynamic GSP pages pre-render for each locale? +export const getStaticProps = ({ locale, locales }) => { + return { + props: { + locale, + locales, + }, + }; +}; diff --git a/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/gsp/no-fallback/[slug].js b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/gsp/no-fallback/[slug].js new file mode 100644 index 00000000000..6d4d776b768 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/gsp/no-fallback/[slug].js @@ -0,0 +1,46 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + + if (router.isFallback) return 'Loading...'; + + return ( + <> +

gsp page

+

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + +
+ + ); +} + +export const getStaticProps = ({ params, locale, locales }) => { + return { + props: { + params, + locale, + locales, + }, + }; +}; + +export const getStaticPaths = () => { + return { + paths: [ + { params: { slug: 'first' } }, + '/gsp/no-fallback/second', + { params: { slug: 'first' }, locale: 'en-US' }, + '/nl-NL/gsp/no-fallback/second', + ], + fallback: false, + }; +}; diff --git a/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/gssp/[slug].js b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/gssp/[slug].js new file mode 100644 index 00000000000..d1cede7209b --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/gssp/[slug].js @@ -0,0 +1,32 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + + return ( + <> +

gssp page

+

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + +
+ + ); +} + +export const getServerSideProps = ({ params, locale, locales }) => { + return { + props: { + params, + locale, + locales, + }, + }; +}; diff --git a/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/gssp/index.js b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/gssp/index.js new file mode 100644 index 00000000000..ab02dea58c9 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/gssp/index.js @@ -0,0 +1,31 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + + return ( + <> +

gssp page

+

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + +
+ + ); +} + +export const getServerSideProps = ({ locale, locales }) => { + return { + props: { + locale, + locales, + }, + }; +}; diff --git a/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/index.js b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/index.js new file mode 100644 index 00000000000..1467602e631 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/index.js @@ -0,0 +1,46 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + + return ( + <> +

index page

+

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to /another + +
+ + to /gsp + +
+ + to /gsp/fallback/first + +
+ + to /gsp/fallback/hello + +
+ + to /gsp/no-fallback/first + +
+ + to /gssp + +
+ + to /gssp/first + +
+ + ); +} diff --git a/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/links.js b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/links.js new file mode 100644 index 00000000000..5cb380c28a4 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/pages/links.js @@ -0,0 +1,54 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + const { nextLocale } = router.query; + + return ( + <> + +

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to /another + +
+ + to /gsp + +
+ + to /gsp/fallback/first + +
+ + to /gsp/fallback/hello + +
+ + to /gsp/no-fallback/first + +
+ + to /gssp + +
+ + to /gssp/first + +
+ + ); +} + +// make SSR page so we have query values immediately +export const getServerSideProps = () => { + return { + props: {}, + }; +}; diff --git a/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/public/hello.txt b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/public/hello.txt new file mode 100644 index 00000000000..bc7774a7b18 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support-no-shared-lambdas/public/hello.txt @@ -0,0 +1 @@ +hello world! \ No newline at end of file diff --git a/packages/now-next/test/fixtures/00-i18n-support/next.config.js b/packages/now-next/test/fixtures/00-i18n-support/next.config.js new file mode 100644 index 00000000000..4f05fa39ab8 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support/next.config.js @@ -0,0 +1,20 @@ +module.exports = { + experimental: { + i18n: { + locales: ['nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en-US', 'en'], + defaultLocale: 'en-US', + // TODO: testing locale domains support, will require custom + // testing set-up as test accounts are used currently + domains: [ + { + domain: 'example.be', + defaultLocale: 'nl-BE', + }, + { + domain: 'example.fr', + defaultLocale: 'fr', + }, + ], + }, + }, +}; diff --git a/packages/now-next/test/fixtures/00-i18n-support/now.json b/packages/now-next/test/fixtures/00-i18n-support/now.json new file mode 100644 index 00000000000..8ca9d6b615b --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support/now.json @@ -0,0 +1,315 @@ +{ + "version": 2, + "builds": [ + { + "src": "package.json", + "use": "@vercel/next" + } + ], + "probes": [ + { + "path": "/", + "headers": { + "accept-language": "en;q=0.9" + }, + "fetchOptions": { + "redirect": "manual" + }, + "status": 307, + "responseHeaders": { + "location": "//en/" + } + }, + { + "path": "/", + "headers": { + "accept-language": "nl;q=0.9" + }, + "fetchOptions": { + "redirect": "manual" + }, + "status": 307, + "responseHeaders": { + "location": "//nl/" + } + }, + { + "path": "/", + "headers": { + "accept-language": "nl-NL;q=0.9" + }, + "fetchOptions": { + "redirect": "manual" + }, + "status": 307, + "responseHeaders": { + "location": "//nl-NL/" + } + }, + { + "path": "/", + "headers": { + "accept-language": "fr;q=0.9" + }, + "fetchOptions": { + "redirect": "manual" + }, + "status": 307, + "responseHeaders": { + "location": "//fr/" + } + }, + { + "path": "/", + "headers": { + "accept-language": "en-US;q=0.9" + }, + "fetchOptions": { + "redirect": "manual" + }, + "status": 200, + "mustContain": "index page" + }, + { + "path": "/en-US", + "headers": { + "accept-language": "nl;q=0.9" + }, + "fetchOptions": { + "redirect": "manual" + }, + "status": 200, + "mustContain": "index page" + }, + + { + "path": "/", + "status": 200, + "mustContain": "index page" + }, + { + "path": "/", + "status": 200, + "mustContain": ">en-US<" + }, + { + "path": "/en", + "status": 200, + "mustContain": "index page" + }, + { + "path": "/en", + "status": 200, + "mustContain": ">en<" + }, + { + "path": "/fr", + "status": 200, + "mustContain": "index page" + }, + { + "path": "/fr", + "status": 200, + "mustContain": ">fr<" + }, + { + "path": "/nl", + "status": 200, + "mustContain": "index page" + }, + { + "path": "/nl", + "status": 200, + "mustContain": ">nl<" + }, + { + "path": "/nl-NL", + "status": 200, + "mustContain": "index page" + }, + { + "path": "/nl-NL", + "status": 200, + "mustContain": ">nl-NL<" + }, + + { + "path": "/non-existent", + "status": 404 + }, + { + "path": "/fr/non-existent", + "status": 404, + "mustContain": "lang=\"fr\"" + }, + { + "path": "/en/non-existent", + "status": 404, + "mustContain": "lang=\"en\"" + }, + { + "path": "/en-US/non-existent", + "status": 404, + "mustContain": "lang=\"en-US\"" + }, + { + "path": "/nl/non-existent", + "status": 404, + "mustContain": "lang=\"nl\"" + }, + { + "path": "/nl-NL/non-existent", + "status": 404, + "mustContain": "lang=\"nl-NL\"" + }, + + { + "path": "/hello.txt", + "status": 200, + "mustContain": "hello world!" + }, + + { + "path": "/gsp", + "status": 200, + "mustContain": "gsp page" + }, + { + "path": "/gsp", + "status": 200, + "mustContain": ">en-US<" + }, + { + "path": "/en/gsp", + "status": 200, + "mustContain": "gsp page" + }, + { + "path": "/en/gsp", + "status": 200, + "mustContain": ">en<" + }, + { + "path": "/nl/gsp", + "status": 200, + "mustContain": "gsp page" + }, + { + "path": "/nl/gsp", + "status": 200, + "mustContain": ">nl<" + }, + { + "path": "/fr/gsp", + "status": 200, + "mustContain": "gsp page" + }, + { + "path": "/fr/gsp", + "status": 200, + "mustContain": ">fr<" + }, + + { + "path": "/gssp", + "status": 200, + "mustContain": "gssp page" + }, + { + "path": "/gssp", + "status": 200, + "mustContain": ">en-US<" + }, + { + "path": "/en/gssp", + "status": 200, + "mustContain": "gssp page" + }, + { + "path": "/en/gssp", + "status": 200, + "mustContain": ">en<" + }, + { + "path": "/nl/gssp", + "status": 200, + "mustContain": "gssp page" + }, + { + "path": "/nl/gssp", + "status": 200, + "mustContain": ">nl<" + }, + { + "path": "/fr/gssp", + "status": 200, + "mustContain": "gssp page" + }, + { + "path": "/fr/gssp", + "status": 200, + "mustContain": ">fr<" + }, + + { + "path": "/gssp/first", + "status": 200, + "mustContain": "gssp page" + }, + { + "path": "/gssp/first", + "status": 200, + "mustContain": ">en-US<" + }, + { + "path": "/gssp/first", + "status": 200, + "mustContain": "slug\":\"first\"" + }, + { + "path": "/en/gssp/first", + "status": 200, + "mustContain": "gssp page" + }, + { + "path": "/en/gssp/first", + "status": 200, + "mustContain": ">en<" + }, + { + "path": "/en/gssp/first", + "status": 200, + "mustContain": "slug\":\"first\"" + }, + { + "path": "/nl/gssp/first", + "status": 200, + "mustContain": "gssp page" + }, + { + "path": "/nl/gssp/first", + "status": 200, + "mustContain": ">nl<" + }, + { + "path": "/nl/gssp/first", + "status": 200, + "mustContain": "slug\":\"first\"" + }, + { + "path": "/fr/gssp/first", + "status": 200, + "mustContain": "gssp page" + }, + { + "path": "/fr/gssp/first", + "status": 200, + "mustContain": ">fr<" + }, + { + "path": "/fr/gssp/first", + "status": 200, + "mustContain": "slug\":\"first\"" + } + ] +} diff --git a/packages/now-next/test/fixtures/00-i18n-support/package.json b/packages/now-next/test/fixtures/00-i18n-support/package.json new file mode 100644 index 00000000000..80dcb99fc31 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "next": "canary", + "react": "^16.8.6", + "react-dom": "^16.8.6" + } +} diff --git a/packages/now-next/test/fixtures/00-i18n-support/pages/another.js b/packages/now-next/test/fixtures/00-i18n-support/pages/another.js new file mode 100644 index 00000000000..23d80fdad94 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support/pages/another.js @@ -0,0 +1,31 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + + return ( + <> +

another page

+

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + +
+ + ); +} + +export const getServerSideProps = ({ locale, locales }) => { + return { + props: { + locale, + locales, + }, + }; +}; diff --git a/packages/now-next/test/fixtures/00-i18n-support/pages/auto-export/index.js b/packages/now-next/test/fixtures/00-i18n-support/pages/auto-export/index.js new file mode 100644 index 00000000000..bcb719b2bdd --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support/pages/auto-export/index.js @@ -0,0 +1,21 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + + return ( + <> +

auto-export page

+

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + + + ); +} diff --git a/packages/now-next/test/fixtures/00-i18n-support/pages/gsp/fallback/[slug].js b/packages/now-next/test/fixtures/00-i18n-support/pages/gsp/fallback/[slug].js new file mode 100644 index 00000000000..387aad4d153 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support/pages/gsp/fallback/[slug].js @@ -0,0 +1,44 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + + if (router.isFallback) return 'Loading...'; + + return ( + <> +

gsp page

+

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + +
+ + ); +} + +export const getStaticProps = ({ params, locale, locales }) => { + return { + props: { + params, + locale, + locales, + }, + }; +}; + +export const getStaticPaths = () => { + return { + // the default locale will be used since one isn't defined here + paths: ['first', 'second'].map(slug => ({ + params: { slug }, + })), + fallback: true, + }; +}; diff --git a/packages/now-next/test/fixtures/00-i18n-support/pages/gsp/index.js b/packages/now-next/test/fixtures/00-i18n-support/pages/gsp/index.js new file mode 100644 index 00000000000..7b62978db77 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support/pages/gsp/index.js @@ -0,0 +1,32 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + + return ( + <> +

gsp page

+

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + +
+ + ); +} + +// TODO: should non-dynamic GSP pages pre-render for each locale? +export const getStaticProps = ({ locale, locales }) => { + return { + props: { + locale, + locales, + }, + }; +}; diff --git a/packages/now-next/test/fixtures/00-i18n-support/pages/gsp/no-fallback/[slug].js b/packages/now-next/test/fixtures/00-i18n-support/pages/gsp/no-fallback/[slug].js new file mode 100644 index 00000000000..6d4d776b768 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support/pages/gsp/no-fallback/[slug].js @@ -0,0 +1,46 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + + if (router.isFallback) return 'Loading...'; + + return ( + <> +

gsp page

+

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + +
+ + ); +} + +export const getStaticProps = ({ params, locale, locales }) => { + return { + props: { + params, + locale, + locales, + }, + }; +}; + +export const getStaticPaths = () => { + return { + paths: [ + { params: { slug: 'first' } }, + '/gsp/no-fallback/second', + { params: { slug: 'first' }, locale: 'en-US' }, + '/nl-NL/gsp/no-fallback/second', + ], + fallback: false, + }; +}; diff --git a/packages/now-next/test/fixtures/00-i18n-support/pages/gssp/[slug].js b/packages/now-next/test/fixtures/00-i18n-support/pages/gssp/[slug].js new file mode 100644 index 00000000000..d1cede7209b --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support/pages/gssp/[slug].js @@ -0,0 +1,32 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + + return ( + <> +

gssp page

+

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + +
+ + ); +} + +export const getServerSideProps = ({ params, locale, locales }) => { + return { + props: { + params, + locale, + locales, + }, + }; +}; diff --git a/packages/now-next/test/fixtures/00-i18n-support/pages/gssp/index.js b/packages/now-next/test/fixtures/00-i18n-support/pages/gssp/index.js new file mode 100644 index 00000000000..ab02dea58c9 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support/pages/gssp/index.js @@ -0,0 +1,31 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + + return ( + <> +

gssp page

+

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + +
+ + ); +} + +export const getServerSideProps = ({ locale, locales }) => { + return { + props: { + locale, + locales, + }, + }; +}; diff --git a/packages/now-next/test/fixtures/00-i18n-support/pages/index.js b/packages/now-next/test/fixtures/00-i18n-support/pages/index.js new file mode 100644 index 00000000000..1467602e631 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support/pages/index.js @@ -0,0 +1,46 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + + return ( + <> +

index page

+

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to /another + +
+ + to /gsp + +
+ + to /gsp/fallback/first + +
+ + to /gsp/fallback/hello + +
+ + to /gsp/no-fallback/first + +
+ + to /gssp + +
+ + to /gssp/first + +
+ + ); +} diff --git a/packages/now-next/test/fixtures/00-i18n-support/pages/links.js b/packages/now-next/test/fixtures/00-i18n-support/pages/links.js new file mode 100644 index 00000000000..5cb380c28a4 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support/pages/links.js @@ -0,0 +1,54 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +export default function Page(props) { + const router = useRouter(); + const { nextLocale } = router.query; + + return ( + <> + +

{JSON.stringify(props)}

+

{router.locale}

+

{JSON.stringify(router.locales)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to /another + +
+ + to /gsp + +
+ + to /gsp/fallback/first + +
+ + to /gsp/fallback/hello + +
+ + to /gsp/no-fallback/first + +
+ + to /gssp + +
+ + to /gssp/first + +
+ + ); +} + +// make SSR page so we have query values immediately +export const getServerSideProps = () => { + return { + props: {}, + }; +}; diff --git a/packages/now-next/test/fixtures/00-i18n-support/public/hello.txt b/packages/now-next/test/fixtures/00-i18n-support/public/hello.txt new file mode 100644 index 00000000000..bc7774a7b18 --- /dev/null +++ b/packages/now-next/test/fixtures/00-i18n-support/public/hello.txt @@ -0,0 +1 @@ +hello world! \ No newline at end of file diff --git a/packages/now-routing-utils/src/types.ts b/packages/now-routing-utils/src/types.ts index 26fcefc5328..295d4a3a470 100644 --- a/packages/now-routing-utils/src/types.ts +++ b/packages/now-routing-utils/src/types.ts @@ -17,6 +17,10 @@ export type Source = { continue?: boolean; check?: boolean; status?: number; + locale?: { + redirect: Record; + cookie: string; + }; }; export type Handler = { diff --git a/yarn.lock b/yarn.lock index 9d88c35f0d8..6da91f3c834 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4793,21 +4793,16 @@ escape-html@1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-3.0.0.tgz#1dad9cc28aed682be0de197280f79911a5fccd61" - integrity sha512-11dXIUC3umvzEViLP117d0KN6LJzZxh5+9F4E/7WLAAw7GrHk8NpUR+g9iJi/pe9C0py4F8rs0hreyRCwlAuZg== +escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - escodegen@^1.8.0, escodegen@^1.9.1: version "1.14.2" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.2.tgz#14ab71bf5026c2aa08173afba22c6f3173284a84"