Skip to content

Commit

Permalink
Handle preferring default locale over accept-lang preferred locale (#…
Browse files Browse the repository at this point in the history
…17883)

This updates to set the `NEXT_LOCALE` cookie to the default locale when the user prefers a different locale from the default in their `accept-language` header but visits the default locale path e.g. `/en-US` with a `accept-language` preferred header of `nl` will set the `NEXT_LOCALE=en-US` header and then redirect to `/`

x-ref: #17370
  • Loading branch information
ijjk committed Oct 14, 2020
1 parent 7cf7c2f commit 245499a
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 12 deletions.
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

0 comments on commit 245499a

Please sign in to comment.