From bbc1a21c749c423e842586ab116889c9f9c7024e Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 8 Oct 2020 06:12:17 -0500 Subject: [PATCH] Update to have default locale matched on root (#17669) Follow-up PR to https://github.com/vercel/next.js/pull/17370 when the path is not prefixed with a locale and the default locale is the detected locale it doesn't redirect to locale prefixed variant. If the default locale path is visited and the default locale is visited this also redirects to the root removing the un-necessary locale in the URL. This also exposes the `defaultLocale` on the router since the RFC mentions `Setting a defaultLocale is required in every i18n library so it'd be useful for Next.js to provide it to the application.` although doesn't explicitly spec where we want to expose it. If we want to expose it differently this can be updated. --- .../webpack/loaders/next-serverless-loader.ts | 17 +- packages/next/client/index.tsx | 2 + packages/next/client/link.tsx | 4 +- packages/next/client/page-loader.ts | 29 ++- packages/next/client/router.ts | 1 + packages/next/export/index.ts | 7 +- .../lib/i18n/normalize-locale-path.ts | 8 +- .../next/next-server/lib/router/router.ts | 32 ++- packages/next/next-server/lib/utils.ts | 1 + packages/next/next-server/server/config.ts | 23 ++ .../next/next-server/server/next-server.ts | 41 ++-- packages/next/next-server/server/render.tsx | 11 +- test/integration/i18n-support/next.config.js | 2 +- .../i18n-support/test/index.test.js | 203 ++++++++++++++++-- test/lib/next-webdriver.js | 1 + 15 files changed, 331 insertions(+), 51 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index d531cf46cd1124c..2621f4100528516 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -229,24 +229,34 @@ const nextServerlessLoader: loader.Loader = function () { detectedLocale = accept.language( req.headers['accept-language'], i18n.locales - ) || i18n.defaultLocale + ) } + const denormalizedPagePath = denormalizePagePath(parsedUrl.pathname || '/') + const detectedDefaultLocale = detectedLocale === i18n.defaultLocale + const shouldStripDefaultLocale = + detectedDefaultLocale && + denormalizedPagePath === \`/\${i18n.defaultLocale}\` + const shouldAddLocalePrefix = + !detectedDefaultLocale && denormalizedPagePath === '/' + detectedLocale = detectedLocale || i18n.defaultLocale + if ( !nextStartMode && i18n.localeDetection !== false && - denormalizePagePath(parsedUrl.pathname || '/') === '/' + (shouldAddLocalePrefix || shouldStripDefaultLocale) ) { res.setHeader( 'Location', formatUrl({ // make sure to include any query values when redirecting ...parsedUrl, - pathname: \`/\${detectedLocale}\`, + pathname: shouldStripDefaultLocale ? '/' : \`/\${detectedLocale}\`, }) ) res.statusCode = 307 res.end() + return } // TODO: domain based locales (domain to locale mapping needs to be provided in next.config.js) @@ -458,6 +468,7 @@ const nextServerlessLoader: loader.Loader = function () { isDataReq: _nextData, locale: detectedLocale, locales: i18n.locales, + defaultLocale: i18n.defaultLocale, }, options, ) diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index bd148864714498e..e511d8abb294940 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -65,6 +65,7 @@ const { isFallback, head: initialHeadData, locales, + defaultLocale, } = data let { locale } = data @@ -317,6 +318,7 @@ export default async (opts: { webpackHMR?: any } = {}) => { render({ App, Component, styleSheets, props, err }), locale, locales, + defaultLocale, }) // call init-client middleware diff --git a/packages/next/client/link.tsx b/packages/next/client/link.tsx index 76b59444dde7081..56da5f68c4c3c38 100644 --- a/packages/next/client/link.tsx +++ b/packages/next/client/link.tsx @@ -332,7 +332,9 @@ function Link(props: React.PropsWithChildren) { // If child is an tag and doesn't have a href attribute, or if the 'passHref' property is // defined, we specify the current 'href', so that repetition is not needed by the user if (props.passHref || (child.type === 'a' && !('href' in child.props))) { - childProps.href = addBasePath(addLocale(as, router && router.locale)) + childProps.href = addBasePath( + addLocale(as, router && router.locale, router && router.defaultLocale) + ) } return React.cloneElement(child, childProps) diff --git a/packages/next/client/page-loader.ts b/packages/next/client/page-loader.ts index 57e8c3374bfb72d..f53dc9a1a61713e 100644 --- a/packages/next/client/page-loader.ts +++ b/packages/next/client/page-loader.ts @@ -203,13 +203,23 @@ export default class PageLoader { * @param {string} href the route href (file-system path) * @param {string} asPath the URL as shown in browser (virtual path); used for dynamic routes */ - getDataHref(href: string, asPath: string, ssg: boolean, locale?: string) { + getDataHref( + href: string, + asPath: string, + ssg: boolean, + locale?: string, + defaultLocale?: string + ) { const { pathname: hrefPathname, query, search } = parseRelativeUrl(href) const { pathname: asPathname } = parseRelativeUrl(asPath) const route = normalizeRoute(hrefPathname) const getHrefForSlug = (path: string) => { - const dataRoute = addLocale(getAssetPathFromRoute(path, '.json'), locale) + const dataRoute = addLocale( + getAssetPathFromRoute(path, '.json'), + locale, + defaultLocale + ) return addBasePath( `/_next/data/${this.buildId}${dataRoute}${ssg ? '' : search}` ) @@ -229,7 +239,12 @@ export default class PageLoader { * @param {string} href the route href (file-system path) * @param {string} asPath the URL as shown in browser (virtual path); used for dynamic routes */ - prefetchData(href: string, asPath: string) { + prefetchData( + href: string, + asPath: string, + locale?: string, + defaultLocale?: string + ) { const { pathname: hrefPathname } = parseRelativeUrl(href) const route = normalizeRoute(hrefPathname) return this.promisedSsgManifest!.then( @@ -237,7 +252,13 @@ export default class PageLoader { // Check if the route requires a data file s.has(route) && // Try to generate data href, noop when falsy - (_dataHref = this.getDataHref(href, asPath, true)) && + (_dataHref = this.getDataHref( + href, + asPath, + true, + locale, + defaultLocale + )) && // noop when data has already been prefetched (dedupe) !document.querySelector( `link[rel="${relPrefetch}"][href^="${_dataHref}"]` diff --git a/packages/next/client/router.ts b/packages/next/client/router.ts index 54a3f65b378f7fa..cff79525bd50f37 100644 --- a/packages/next/client/router.ts +++ b/packages/next/client/router.ts @@ -39,6 +39,7 @@ const urlPropertyFields = [ 'basePath', 'locale', 'locales', + 'defaultLocale', ] const routerEvents = [ 'routeChangeStart', diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index 056f86c5fb8ee10..cc5f14a7a5ffc00 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -283,6 +283,8 @@ export default async function exportApp( } } + const { i18n } = nextConfig.experimental + // Start the rendering process const renderOpts = { dir, @@ -298,8 +300,9 @@ export default async function exportApp( ampValidatorPath: nextConfig.experimental.amp?.validator || undefined, ampSkipValidation: nextConfig.experimental.amp?.skipValidation || false, ampOptimizerConfig: nextConfig.experimental.amp?.optimizer || undefined, - locales: nextConfig.experimental.i18n?.locales, - locale: nextConfig.experimental.i18n?.defaultLocale, + locales: i18n?.locales, + locale: i18n.defaultLocale, + defaultLocale: i18n.defaultLocale, } const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig diff --git a/packages/next/next-server/lib/i18n/normalize-locale-path.ts b/packages/next/next-server/lib/i18n/normalize-locale-path.ts index ca88a7277c04daa..a46bb4733407364 100644 --- a/packages/next/next-server/lib/i18n/normalize-locale-path.ts +++ b/packages/next/next-server/lib/i18n/normalize-locale-path.ts @@ -6,10 +6,14 @@ export function normalizeLocalePath( 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 (pathname.startsWith(`/${locale}`)) { + if (pathnameParts[1] === locale) { detectedLocale = locale - pathname = pathname.replace(new RegExp(`^/${locale}`), '') || '/' + pathnameParts.splice(1, 1) + pathname = pathnameParts.join('/') || '/' return true } return false diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index f2614c4545ce378..c6d0916aedd2f06 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -55,9 +55,13 @@ function addPathPrefix(path: string, prefix?: string) { : path } -export function addLocale(path: string, locale?: string) { +export function addLocale( + path: string, + locale?: string, + defaultLocale?: string +) { if (process.env.__NEXT_i18n_SUPPORT) { - return locale && !path.startsWith('/' + locale) + return locale && locale !== defaultLocale && !path.startsWith('/' + locale) ? addPathPrefix(path, '/' + locale) : path } @@ -246,6 +250,7 @@ export type BaseRouter = { basePath: string locale?: string locales?: string[] + defaultLocale?: string } export type NextRouter = BaseRouter & @@ -356,6 +361,7 @@ export default class Router implements BaseRouter { _shallow?: boolean locale?: string locales?: string[] + defaultLocale?: string static events: MittEmitter = mitt() @@ -375,6 +381,7 @@ export default class Router implements BaseRouter { isFallback, locale, locales, + defaultLocale, }: { subscription: Subscription initialProps: any @@ -387,6 +394,7 @@ export default class Router implements BaseRouter { isFallback: boolean locale?: string locales?: string[] + defaultLocale?: string } ) { // represents the current component key @@ -440,6 +448,7 @@ export default class Router implements BaseRouter { if (process.env.__NEXT_i18n_SUPPORT) { this.locale = locale this.locales = locales + this.defaultLocale = defaultLocale } if (typeof window !== 'undefined') { @@ -596,7 +605,7 @@ export default class Router implements BaseRouter { this.abortComponentLoad(this._inFlightRoute) } - as = addLocale(as, this.locale) + as = addLocale(as, this.locale, this.defaultLocale) const cleanedAs = delLocale( hasBasePath(as) ? delBasePath(as) : as, this.locale @@ -790,7 +799,12 @@ export default class Router implements BaseRouter { } Router.events.emit('beforeHistoryChange', as) - this.changeState(method, url, addLocale(as, this.locale), options) + this.changeState( + method, + url, + addLocale(as, this.locale, this.defaultLocale), + options + ) if (process.env.NODE_ENV !== 'production') { const appComp: any = this.components['/_app'].Component @@ -960,7 +974,8 @@ export default class Router implements BaseRouter { formatWithValidation({ pathname, query }), delBasePath(as), __N_SSG, - this.locale + this.locale, + this.defaultLocale ) } @@ -1117,7 +1132,12 @@ export default class Router implements BaseRouter { const route = removePathTrailingSlash(pathname) await Promise.all([ - this.pageLoader.prefetchData(url, asPath), + this.pageLoader.prefetchData( + url, + asPath, + this.locale, + this.defaultLocale + ), this.pageLoader[options.priority ? 'loadPage' : 'prefetch'](route), ]) } diff --git a/packages/next/next-server/lib/utils.ts b/packages/next/next-server/lib/utils.ts index a65c74eabd1f44c..057188def881c5d 100644 --- a/packages/next/next-server/lib/utils.ts +++ b/packages/next/next-server/lib/utils.ts @@ -103,6 +103,7 @@ export type NEXT_DATA = { head: HeadEntry[] locale?: string locales?: string[] + defaultLocale?: string } /** diff --git a/packages/next/next-server/server/config.ts b/packages/next/next-server/server/config.ts index 90d8659dbd54669..867433dfbd160e5 100644 --- a/packages/next/next-server/server/config.ts +++ b/packages/next/next-server/server/config.ts @@ -227,12 +227,35 @@ function assignDefaults(userConfig: { [key: string]: any }) { throw new Error(`Specified i18n.defaultLocale should be a string`) } + if (!Array.isArray(i18n.locales)) { + throw new Error( + `Specified i18n.locales must be an array of locale strings e.g. ["en-US", "nl-NL"] received ${typeof i18n.locales}` + ) + } + + const invalidLocales = i18n.locales.filter( + (locale: any) => typeof locale !== 'string' + ) + + if (invalidLocales.length > 0) { + throw new Error( + `Specified i18n.locales contains invalid values, locales must be valid locale tags provided as strings e.g. "en-US".\n` + + `See here for list of valid language sub-tags: http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry` + ) + } + if (!i18n.locales.includes(i18n.defaultLocale)) { throw new Error( `Specified i18n.defaultLocale should be included in i18n.locales` ) } + // make sure default Locale is at the front + i18n.locales = [ + i18n.defaultLocale, + ...i18n.locales.filter((locale: string) => locale !== i18n.defaultLocale), + ] + const localeDetectionType = typeof i18n.locales.localeDetection if ( diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index b920d7fc45957b0..03e5c8f3bd76fd9 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -140,6 +140,7 @@ export default class Server { optimizeImages: boolean locale?: string locales?: string[] + defaultLocale?: string } private compression?: Middleware private onErrorMiddleware?: ({ err }: { err: Error }) => Promise @@ -193,6 +194,7 @@ export default class Server { : null, optimizeImages: this.nextConfig.experimental.optimizeImages, locales: this.nextConfig.experimental.i18n?.locales, + defaultLocale: this.nextConfig.experimental.i18n?.defaultLocale, } // Only the `publicRuntimeConfig` key is exposed to the client side @@ -302,31 +304,42 @@ export default class Server { req.url = req.url!.replace(basePath, '') || '/' } - if (i18n) { + if (i18n && !parsedUrl.pathname?.startsWith('/_next')) { // get pathname from URL with basePath stripped for locale detection const { pathname, ...parsed } = parseUrl(req.url || '/') let detectedLocale = detectLocaleCookie(req, i18n.locales) if (!detectedLocale) { - detectedLocale = - accept.language(req.headers['accept-language'], i18n.locales) || - i18n.defaultLocale + detectedLocale = accept.language( + req.headers['accept-language'], + i18n.locales + ) } + const denormalizedPagePath = denormalizePagePath(pathname || '/') + const detectedDefaultLocale = detectedLocale === i18n.defaultLocale + const shouldStripDefaultLocale = + detectedDefaultLocale && + denormalizedPagePath === `/${i18n.defaultLocale}` + const shouldAddLocalePrefix = + !detectedDefaultLocale && denormalizedPagePath === '/' + detectedLocale = detectedLocale || i18n.defaultLocale + if ( i18n.localeDetection !== false && - denormalizePagePath(pathname || '/') === '/' + (shouldAddLocalePrefix || shouldStripDefaultLocale) ) { res.setHeader( 'Location', formatUrl({ // make sure to include any query values when redirecting ...parsed, - pathname: `/${detectedLocale}`, + pathname: shouldStripDefaultLocale ? '/' : `/${detectedLocale}`, }) ) res.statusCode = 307 res.end() + return } // TODO: domain based locales (domain to locale mapping needs to be provided in next.config.js) @@ -487,21 +500,17 @@ export default class Server { // re-create page's pathname let pathname = `/${params.path.join('/')}` - if (this.nextConfig.experimental.i18n) { - const localePathResult = normalizeLocalePath( - pathname, - this.renderOpts.locales - ) - let detectedLocale = detectLocaleCookie( - req, - this.renderOpts.locales! - ) + const { i18n } = this.nextConfig.experimental + + if (i18n) { + const localePathResult = normalizeLocalePath(pathname, i18n.locales) + let detectedLocale = detectLocaleCookie(req, i18n.locales) if (localePathResult.detectedLocale) { pathname = localePathResult.pathname detectedLocale = localePathResult.detectedLocale } - ;(req as any)._nextLocale = detectedLocale + ;(req as any)._nextLocale = detectedLocale || i18n.defaultLocale } pathname = getRouteFromAssetPath(pathname, '.json') diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index 22d45c812b26e78..b8092979093187e 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -69,6 +69,7 @@ class ServerRouter implements NextRouter { isFallback: boolean locale?: string locales?: string[] + defaultLocale?: string // TODO: Remove in the next major version, as this would mean the user is adding event listeners in server-side `render` method static events: MittEmitter = mitt() @@ -79,7 +80,8 @@ class ServerRouter implements NextRouter { { isFallback }: { isFallback: boolean }, basePath: string, locale?: string, - locales?: string[] + locales?: string[], + defaultLocale?: string ) { this.route = pathname.replace(/\/$/, '') || '/' this.pathname = pathname @@ -89,6 +91,7 @@ class ServerRouter implements NextRouter { this.basePath = basePath this.locale = locale this.locales = locales + this.defaultLocale = defaultLocale } push(): any { noRouter() @@ -164,6 +167,7 @@ export type RenderOptsPartial = { resolvedAsPath?: string locale?: string locales?: string[] + defaultLocale?: string } export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial @@ -203,6 +207,7 @@ function renderDocument( devOnlyCacheBusterQueryString, locale, locales, + defaultLocale, }: RenderOpts & { props: any docComponentsRendered: DocumentProps['docComponentsRendered'] @@ -251,6 +256,7 @@ function renderDocument( appGip, // whether the _app has getInitialProps locale, locales, + defaultLocale, head: React.Children.toArray(docProps.head || []) .map((elem) => { const { children } = elem?.props @@ -517,7 +523,8 @@ export async function renderToHTML( }, basePath, renderOpts.locale, - renderOpts.locales + renderOpts.locales, + renderOpts.defaultLocale ) const ctx = { err, diff --git a/test/integration/i18n-support/next.config.js b/test/integration/i18n-support/next.config.js index 1fb0f8120d470a0..d759a91263b9fb8 100644 --- a/test/integration/i18n-support/next.config.js +++ b/test/integration/i18n-support/next.config.js @@ -3,7 +3,7 @@ module.exports = { experimental: { i18n: { locales: ['nl-NL', 'nl-BE', 'nl', 'en-US', 'en'], - defaultLocale: 'en', + defaultLocale: 'en-US', }, }, } diff --git a/test/integration/i18n-support/test/index.test.js b/test/integration/i18n-support/test/index.test.js index a09d7aaea3f326a..dff7a80ad0ddf2b 100644 --- a/test/integration/i18n-support/test/index.test.js +++ b/test/integration/i18n-support/test/index.test.js @@ -24,9 +24,82 @@ let app let appPort // let buildId -const locales = ['nl-NL', 'nl-BE', 'nl', 'en-US', 'en'] +const locales = ['en-US', 'nl-NL', 'nl-BE', 'nl', 'en'] function runTests() { + it('should remove un-necessary locale prefix for default locale', async () => { + const res = await fetchViaHTTP(appPort, '/en-US', undefined, { + redirect: 'manual', + headers: { + 'Accept-Language': 'en-US;q=0.9', + }, + }) + + expect(res.status).toBe(307) + + const parsedUrl = url.parse(res.headers.get('location'), true) + + expect(parsedUrl.pathname).toBe('/') + expect(parsedUrl.query).toEqual({}) + }) + + it('should load getStaticProps page correctly SSR (default locale no prefix)', async () => { + const html = await renderViaHTTP(appPort, '/gsp') + const $ = cheerio.load(html) + + expect(JSON.parse($('#props').text())).toEqual({ + locale: 'en-US', + locales, + }) + expect($('#router-locale').text()).toBe('en-US') + expect(JSON.parse($('#router-locales').text())).toEqual(locales) + expect($('html').attr('lang')).toBe('en-US') + }) + + it('should load getStaticProps fallback prerender page correctly SSR (default locale no prefix)', async () => { + const html = await renderViaHTTP(appPort, '/gsp/fallback/first') + const $ = cheerio.load(html) + + expect(JSON.parse($('#props').text())).toEqual({ + locale: 'en-US', + locales, + params: { + slug: 'first', + }, + }) + expect(JSON.parse($('#router-query').text())).toEqual({ + slug: 'first', + }) + expect($('#router-locale').text()).toBe('en-US') + expect(JSON.parse($('#router-locales').text())).toEqual(locales) + expect($('html').attr('lang')).toBe('en-US') + }) + + it('should load getStaticProps fallback non-prerender page correctly (default locale no prefix', async () => { + const browser = await webdriver(appPort, '/gsp/fallback/another') + + await browser.waitForElementByCss('#props') + + expect(JSON.parse(await browser.elementByCss('#props').text())).toEqual({ + locale: 'en-US', + locales, + params: { + slug: 'another', + }, + }) + expect( + JSON.parse(await browser.elementByCss('#router-query').text()) + ).toEqual({ + slug: 'another', + }) + // TODO: this will be fixed after the fallback is generated for all locales + // instead of delaying populating the locale on the client + // expect(await browser.elementByCss('#router-locale').text()).toBe('en') + expect( + JSON.parse(await browser.elementByCss('#router-locales').text()) + ).toEqual(locales) + }) + it('should redirect to locale prefixed route for /', async () => { const res = await fetchViaHTTP(appPort, '/', undefined, { redirect: 'manual', @@ -47,14 +120,14 @@ function runTests() { { redirect: 'manual', headers: { - 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Language': 'en;q=0.9', }, } ) expect(res2.status).toBe(307) const parsedUrl2 = url.parse(res2.headers.get('location'), true) - expect(parsedUrl2.pathname).toBe('/en-US') + expect(parsedUrl2.pathname).toBe('/en') expect(parsedUrl2.query).toEqual({ hello: 'world' }) }) @@ -65,7 +138,7 @@ function runTests() { expect(res.status).toBe(307) const parsedUrl = url.parse(res.headers.get('location'), true) - expect(parsedUrl.pathname).toBe('/en') + expect(parsedUrl.pathname).toBe('/en-US') expect(parsedUrl.query).toEqual({}) const res2 = await fetchViaHTTP( @@ -79,7 +152,7 @@ function runTests() { expect(res2.status).toBe(307) const parsedUrl2 = url.parse(res2.headers.get('location'), true) - expect(parsedUrl2.pathname).toBe('/en') + expect(parsedUrl2.pathname).toBe('/en-US') expect(parsedUrl2.query).toEqual({ hello: 'world' }) }) @@ -97,11 +170,11 @@ function runTests() { }) it('should load getStaticProps fallback prerender page correctly SSR', async () => { - const html = await renderViaHTTP(appPort, '/en/gsp/fallback/first') + const html = await renderViaHTTP(appPort, '/en-US/gsp/fallback/first') const $ = cheerio.load(html) expect(JSON.parse($('#props').text())).toEqual({ - locale: 'en', + locale: 'en-US', locales, params: { slug: 'first', @@ -110,9 +183,9 @@ function runTests() { expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first', }) - expect($('#router-locale').text()).toBe('en') + expect($('#router-locale').text()).toBe('en-US') expect(JSON.parse($('#router-locales').text())).toEqual(locales) - expect($('html').attr('lang')).toBe('en') + expect($('html').attr('lang')).toBe('en-US') }) it('should load getStaticProps fallback non-prerender page correctly', async () => { @@ -137,12 +210,112 @@ function runTests() { JSON.parse(await browser.elementByCss('#router-locales').text()) ).toEqual(locales) - // TODO: handle updating locale for fallback pages? + // TODO: this will be fixed after fallback pages are generated + // for all locales // expect( // await browser.elementByCss('html').getAttribute('lang') // ).toBe('en-US') }) + it('should load getServerSideProps page correctly SSR (default locale no prefix)', async () => { + const html = await renderViaHTTP(appPort, '/gssp') + const $ = cheerio.load(html) + + expect(JSON.parse($('#props').text())).toEqual({ + locale: 'en-US', + locales, + }) + expect($('#router-locale').text()).toBe('en-US') + expect(JSON.parse($('#router-locales').text())).toEqual(locales) + expect(JSON.parse($('#router-query').text())).toEqual({}) + expect($('html').attr('lang')).toBe('en-US') + }) + + it('should navigate client side for default locale with no prefix', async () => { + const browser = await webdriver(appPort, '/') + // make sure default locale is used in case browser isn't set to + // favor en-US by default + await browser.manage().addCookie({ name: 'NEXT_LOCALE', value: 'en-US' }) + await browser.get(browser.initUrl) + + const checkIndexValues = async () => { + expect(JSON.parse(await browser.elementByCss('#props').text())).toEqual({ + locale: 'en-US', + locales, + }) + expect(await browser.elementByCss('#router-locale').text()).toBe('en-US') + expect( + JSON.parse(await browser.elementByCss('#router-locales').text()) + ).toEqual(locales) + expect( + JSON.parse(await browser.elementByCss('#router-query').text()) + ).toEqual({}) + expect(await browser.elementByCss('#router-pathname').text()).toBe('/') + expect(await browser.elementByCss('#router-as-path').text()).toBe('/') + expect( + url.parse(await browser.eval(() => window.location.href)).pathname + ).toBe('/') + } + + await checkIndexValues() + + await browser.elementByCss('#to-another').click() + await browser.waitForElementByCss('#another') + + expect(JSON.parse(await browser.elementByCss('#props').text())).toEqual({ + locale: 'en-US', + locales, + }) + expect(await browser.elementByCss('#router-locale').text()).toBe('en-US') + expect( + JSON.parse(await browser.elementByCss('#router-locales').text()) + ).toEqual(locales) + expect( + JSON.parse(await browser.elementByCss('#router-query').text()) + ).toEqual({}) + expect(await browser.elementByCss('#router-pathname').text()).toBe( + '/another' + ) + expect(await browser.elementByCss('#router-as-path').text()).toBe( + '/another' + ) + expect( + url.parse(await browser.eval(() => window.location.href)).pathname + ).toBe('/another') + + await browser.elementByCss('#to-index').click() + await browser.waitForElementByCss('#index') + + await checkIndexValues() + + await browser.elementByCss('#to-gsp').click() + await browser.waitForElementByCss('#gsp') + + expect(JSON.parse(await browser.elementByCss('#props').text())).toEqual({ + locale: 'en-US', + locales, + }) + expect(await browser.elementByCss('#router-locale').text()).toBe('en-US') + expect( + JSON.parse(await browser.elementByCss('#router-locales').text()) + ).toEqual(locales) + expect( + JSON.parse(await browser.elementByCss('#router-query').text()) + ).toEqual({}) + expect(await browser.elementByCss('#router-pathname').text()).toBe('/gsp') + expect(await browser.elementByCss('#router-as-path').text()).toBe('/gsp') + expect( + url.parse(await browser.eval(() => window.location.href)).pathname + ).toBe('/gsp') + + await browser.elementByCss('#to-index').click() + await browser.waitForElementByCss('#index') + + await checkIndexValues() + + await browser.manage().deleteCookie('NEXT_LOCALE') + }) + it('should load getStaticProps fallback non-prerender page another locale correctly', async () => { const browser = await webdriver(appPort, '/nl-NL/gsp/fallback/another') @@ -167,12 +340,12 @@ function runTests() { }) it('should load getStaticProps non-fallback correctly', async () => { - const browser = await webdriver(appPort, '/en/gsp/no-fallback/first') + const browser = await webdriver(appPort, '/en-US/gsp/no-fallback/first') await browser.waitForElementByCss('#props') expect(JSON.parse(await browser.elementByCss('#props').text())).toEqual({ - locale: 'en', + locale: 'en-US', locales, params: { slug: 'first', @@ -183,11 +356,13 @@ function runTests() { ).toEqual({ slug: 'first', }) - expect(await browser.elementByCss('#router-locale').text()).toBe('en') + expect(await browser.elementByCss('#router-locale').text()).toBe('en-US') expect( JSON.parse(await browser.elementByCss('#router-locales').text()) ).toEqual(locales) - expect(await browser.elementByCss('html').getAttribute('lang')).toBe('en') + expect(await browser.elementByCss('html').getAttribute('lang')).toBe( + 'en-US' + ) }) it('should load getStaticProps non-fallback correctly another locale', async () => { diff --git a/test/lib/next-webdriver.js b/test/lib/next-webdriver.js index e3b8e6ed6c2f2b0..9ad87b8b2237532 100644 --- a/test/lib/next-webdriver.js +++ b/test/lib/next-webdriver.js @@ -167,6 +167,7 @@ export default async (appPort, path, waitHydration = true) => { } const url = `http://${deviceIP}:${appPort}${path}` + browser.initUrl = url console.log(`\n> Loading browser with ${url}\n`) await browser.get(url)