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

Handle preferring default locale over accept-lang preferred locale #17883

Merged
merged 1 commit into from Oct 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
38 changes: 32 additions & 6 deletions packages/next/build/webpack/loaders/next-serverless-loader.ts
Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -279,6 +280,7 @@ const nextServerlessLoader: loader.Loader = function () {
const shouldStripDefaultLocale =
detectedDefaultLocale &&
denormalizedPagePath.toLowerCase() === \`/\${i18n.defaultLocale.toLowerCase()}\`

const shouldAddLocalePrefix =
!detectedDefaultLocale && denormalizedPagePath === '/'

Expand All @@ -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({
Expand Down
36 changes: 30 additions & 6 deletions packages/next/next-server/server/next-server.ts
Expand Up @@ -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)

Expand Down Expand Up @@ -310,19 +311,19 @@ 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) {
defaultLocale = detectedDomain.defaultLocale
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)
Expand Down Expand Up @@ -360,6 +361,7 @@ export default class Server {
detectedDefaultLocale &&
denormalizedPagePath.toLowerCase() ===
`/${i18n.defaultLocale.toLowerCase()}`

const shouldAddLocalePrefix =
!detectedDefaultLocale && denormalizedPagePath === '/'

Expand All @@ -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({
Expand Down
37 changes: 37 additions & 0 deletions test/integration/i18n-support/test/index.test.js
Expand Up @@ -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
Expand Down