diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index a637c2e37d5f758..41d173d9840b4ed 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -221,12 +221,17 @@ const nextServerlessLoader: loader.Loader = function () { // 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) + let acceptPreferredLocale = accept.language( + req.headers['accept-language'], + i18n.locales + ) const detectedDomain = detectDomainLocale( i18n.domains, @@ -237,12 +242,8 @@ const nextServerlessLoader: loader.Loader = function () { detectedLocale = defaultLocale } - if (!detectedLocale) { - detectedLocale = accept.language( - req.headers['accept-language'], - i18n.locales - ) - } + // if not domain specific locale use accept-language preferred + detectedLocale = detectedLocale || acceptPreferredLocale let localeDomainRedirect const localePathResult = normalizeLocalePath(parsedUrl.pathname, i18n.locales) @@ -279,6 +280,7 @@ const nextServerlessLoader: loader.Loader = function () { const shouldStripDefaultLocale = detectedDefaultLocale && denormalizedPagePath.toLowerCase() === \`/\${i18n.defaultLocale.toLowerCase()}\` + const shouldAddLocalePrefix = !detectedDefaultLocale && denormalizedPagePath === '/' @@ -294,6 +296,30 @@ const nextServerlessLoader: loader.Loader = function () { shouldStripDefaultLocale ) ) { + // set the NEXT_LOCALE cookie when a user visits the default locale + // with the locale prefix so that they aren't redirected back to + // their accept-language preferred locale + if ( + shouldStripDefaultLocale && + acceptPreferredLocale !== defaultLocale + ) { + const previous = res.getHeader('set-cookie') + + res.setHeader( + 'set-cookie', + [ + ...(typeof previous === 'string' + ? [previous] + : Array.isArray(previous) + ? previous + : []), + cookie.serialize('NEXT_LOCALE', defaultLocale, { + httpOnly: true, + path: '/', + }) + ]) + } + res.setHeader( 'Location', formatUrl({ diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index 92b47661022dedd..fb636ac9b9de406 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -80,6 +80,7 @@ import { normalizeLocalePath } from '../lib/i18n/normalize-locale-path' import { detectLocaleCookie } from '../lib/i18n/detect-locale-cookie' import * as Log from '../../build/output/log' import { detectDomainLocale } from '../lib/i18n/detect-domain-locale' +import cookie from 'next/dist/compiled/cookie' const getCustomRouteMatcher = pathMatch(true) @@ -310,6 +311,10 @@ export default class Server { const { pathname, ...parsed } = parseUrl(req.url || '/') let defaultLocale = i18n.defaultLocale let detectedLocale = detectLocaleCookie(req, i18n.locales) + let acceptPreferredLocale = accept.language( + req.headers['accept-language'], + i18n.locales + ) const detectedDomain = detectDomainLocale(i18n.domains, req) if (detectedDomain) { @@ -317,12 +322,8 @@ export default class Server { detectedLocale = defaultLocale } - if (!detectedLocale) { - detectedLocale = accept.language( - req.headers['accept-language'], - i18n.locales - ) - } + // if not domain specific locale use accept-language preferred + detectedLocale = detectedLocale || acceptPreferredLocale let localeDomainRedirect: string | undefined const localePathResult = normalizeLocalePath(pathname!, i18n.locales) @@ -360,6 +361,7 @@ export default class Server { detectedDefaultLocale && denormalizedPagePath.toLowerCase() === `/${i18n.defaultLocale.toLowerCase()}` + const shouldAddLocalePrefix = !detectedDefaultLocale && denormalizedPagePath === '/' @@ -371,6 +373,28 @@ export default class Server { shouldAddLocalePrefix || shouldStripDefaultLocale) ) { + // set the NEXT_LOCALE cookie when a user visits the default locale + // with the locale prefix so that they aren't redirected back to + // their accept-language preferred locale + if ( + shouldStripDefaultLocale && + acceptPreferredLocale !== defaultLocale + ) { + const previous = res.getHeader('set-cookie') + + res.setHeader('set-cookie', [ + ...(typeof previous === 'string' + ? [previous] + : Array.isArray(previous) + ? previous + : []), + cookie.serialize('NEXT_LOCALE', defaultLocale, { + httpOnly: true, + path: '/', + }), + ]) + } + res.setHeader( 'Location', formatUrl({ diff --git a/test/integration/i18n-support/test/index.test.js b/test/integration/i18n-support/test/index.test.js index 00f1bf163ce457a..c34cdc488221130 100644 --- a/test/integration/i18n-support/test/index.test.js +++ b/test/integration/i18n-support/test/index.test.js @@ -132,6 +132,43 @@ function runTests(isDev) { expect(result2.query).toEqual({}) }) + it('should set locale cookie when removing default locale and accept-lang doesnt match', async () => { + const res = await fetchViaHTTP(appPort, '/en-US', undefined, { + headers: { + 'accept-language': 'nl', + }, + redirect: 'manual', + }) + + expect(res.status).toBe(307) + + const parsedUrl = url.parse(res.headers.get('location'), true) + expect(parsedUrl.pathname).toBe('/') + expect(parsedUrl.query).toEqual({}) + expect(res.headers.get('set-cookie')).toContain('NEXT_LOCALE=en-US') + }) + + it('should not redirect to accept-lang preferred locale with locale cookie', async () => { + const res = await fetchViaHTTP(appPort, '/', undefined, { + headers: { + 'accept-language': 'nl', + cookie: 'NEXT_LOCALE=en-US', + }, + redirect: 'manual', + }) + + expect(res.status).toBe(200) + + const html = await res.text() + const $ = cheerio.load(html) + + expect($('#router-locale').text()).toBe('en-US') + expect(JSON.parse($('#router-locales').text())).toEqual(locales) + expect($('html').attr('lang')).toBe('en-US') + expect($('#router-pathname').text()).toBe('/') + expect($('#router-as-path').text()).toBe('/') + }) + it('should redirect to correct locale domain', async () => { const checks = [ // test domain, locale prefix, redirect result