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

feat: "onlyOnNoPrefix" - detect browser locale when no prefix #896

Merged
merged 8 commits into from
Dec 3, 2020
Merged
1 change: 1 addition & 0 deletions docs/content/en/options-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ Supported properties:
- `alwaysRedirect` (default: `false`) - Set to always redirect to the value stored in the cookie, not just on first visit.
- `fallbackLocale` (default: `null`) - If none of the locales match the browser's locale, use this one as a fallback.
- `onlyOnRoot` (default: `false`) - Set to `true` (recommended for improved SEO) to only attempt to detect the browser locale on the root path (`/`) of the site. Only effective when using strategy other than `'no_prefix'`.
- `onlyOnNoPrefix` (default: `false`) - This is a more permissive variant of `onlyOnRoot` that will allow attempt to detect the browser locale on the root path (`/`) and also on paths that have no locale prefix (like `/foo`). Only effective when `onlyOnRoot` is not enabled and using strategy other than `'no_prefix'`.
- `useCookie` (default: `true`) - If enabled, a cookie is set once the user has been redirected to browser's preferred locale, to prevent subsequent redirections. Set to `false` to redirect every time.
- `cookieKey` (default: `'i18n_redirected'`) - Cookie name.
- `cookieDomain` (default: `null`) - Set to override the default domain of the cookie. Defaults to the **host** of the site.
Expand Down
1 change: 1 addition & 0 deletions docs/content/es/options-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ Supported properties:
- `alwaysRedirect` (default: `false`) - Set to always redirect to the value stored in the cookie, not just on first visit.
- `fallbackLocale` (default: `null`) - If none of the locales match the browser's locale, use this one as a fallback.
- `onlyOnRoot` (default: `false`) - Set to `true` (recommended for improved SEO) to only attempt to detect the browser locale on the root path (`/`) of the site. Only effective when using strategy other than `'no_prefix'`.
- `onlyOnNoPrefix` (default: `false`) - This is a more permissive variant of `onlyOnRoot` that will allow attempt to detect the browser locale on the root path (`/`) and also on paths that have no locale prefix (like `/foo`). Only effective when `onlyOnRoot` is not enabled and using strategy other than `'no_prefix'`.
- `useCookie` (default: `true`) - If enabled, a cookie is set once the user has been redirected to browser's preferred locale, to prevent subsequent redirections. Set to `false` to redirect every time.
- `cookieKey` (default: `'i18n_redirected'`) - Cookie name.
- `cookieDomain` (default: `null`) - Set to override the default domain of the cookie. Defaults to the **host** of the site.
Expand Down
1 change: 1 addition & 0 deletions src/helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ exports.DEFAULT_OPTIONS = {
cookieSecure: false,
alwaysRedirect: false,
fallbackLocale: '',
onlyOnNoPrefix: false,
onlyOnRoot: false
},
differentDomains: false,
Expand Down
19 changes: 14 additions & 5 deletions src/templates/plugin.main.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
createLocaleFromRouteGetter,
getLocaleCookie,
getLocaleDomain,
getLocalesRegex,
resolveBaseUrl,
matchBrowserLocale,
parseAcceptLanguage,
Expand All @@ -38,7 +39,7 @@ import {

Vue.use(VueI18n)

const { alwaysRedirect, onlyOnRoot, fallbackLocale } = detectBrowserLanguage
const { alwaysRedirect, onlyOnNoPrefix, onlyOnRoot, fallbackLocale } = detectBrowserLanguage
const getLocaleFromRoute = createLocaleFromRouteGetter(localeCodes, { routesNameSeparator, defaultLocaleRouteNameSuffix })

/** @type {import('@nuxt/types').Plugin} */
Expand Down Expand Up @@ -125,9 +126,9 @@ export default async (context) => {
}

if (getLocaleFromRoute(route) === locale) {
// If "onlyOnRoot" is set and strategy is "prefix_and_default", prefer unprefixed route for
// If "onlyOnRoot" or "onlyOnNoPrefix" is set and strategy is "prefix_and_default", prefer unprefixed route for
// default locale.
if (!onlyOnRoot || locale !== defaultLocale || strategy !== STRATEGIES.PREFIX_AND_DEFAULT) {
if (!(onlyOnRoot || onlyOnNoPrefix) || locale !== defaultLocale || strategy !== STRATEGIES.PREFIX_AND_DEFAULT) {
return ''
}
}
Expand Down Expand Up @@ -185,8 +186,16 @@ export default async (context) => {
return false
}

if (onlyOnRoot && strategy !== STRATEGIES.NO_PREFIX && route.path !== '/') {
return false
if (strategy !== STRATEGIES.NO_PREFIX) {
if (onlyOnRoot) {
if (route.path !== '/') {
return false
}
} else if (onlyOnNoPrefix) {
if (!alwaysRedirect && route.path.match(getLocalesRegex(localeCodes))) {
return false
}
}
}

let matchedLocale
Expand Down
10 changes: 8 additions & 2 deletions src/templates/utils-common.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ export const getLocaleDomain = (locales, req, { localDomainKey, localeCodeKey })
return null
}

/**
* Creates a RegExp for route paths
* @param {string[]} localeCodes
* @return {RegExp}
*/
export const getLocalesRegex = localeCodes => new RegExp(`^/(${localeCodes.join('|')})(?:/|$)`, 'i')

/**
* Creates getter for getLocaleFromRoute
* @param {string[]} localeCodes
Expand All @@ -113,8 +120,7 @@ export const createLocaleFromRouteGetter = (localeCodes, { routesNameSeparator,
const localesPattern = `(${localeCodes.join('|')})`
const defaultSuffixPattern = `(?:${routesNameSeparator}${defaultLocaleRouteNameSuffix})?`
const regexpName = new RegExp(`${routesNameSeparator}${localesPattern}${defaultSuffixPattern}$`, 'i')
const regexpPath = new RegExp(`^/${localesPattern}/`, 'i')

const regexpPath = getLocalesRegex(localeCodes)
/**
* Extract locale code from given route:
* - If route has a name, try to extract locale from it
Expand Down
126 changes: 126 additions & 0 deletions test/module.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,17 @@ describe('no_prefix strategy', () => {
const dom = getDom(html)
expect(dom.querySelector('#current-locale')?.textContent).toBe('locale: en')
})

test('does detect browser locale', async () => {
const requestOptions = {
headers: {
'Accept-Language': 'fr'
}
}
const html = await get('/', requestOptions)
const dom = getDom(html)
expect(dom.querySelector('#current-locale')?.textContent).toBe('locale: fr')
})
})

describe('no_prefix strategy + differentDomains', () => {
Expand Down Expand Up @@ -1511,6 +1522,107 @@ describe('no_prefix + detectBrowserLanguage + alwaysRedirect', () => {
})
})

describe('prefix + detectBrowserLanguage', () => {
/** @type {Nuxt} */
let nuxt

beforeAll(async () => {
const override = {
i18n: {
defaultLocale: 'fr',
strategy: 'prefix',
detectBrowserLanguage: {
useCookie: true
}
}
}

nuxt = (await setup(loadConfig(__dirname, 'basic', override, { merge: true }))).nuxt
})

afterAll(async () => {
await nuxt.close()
})

test('redirects root even if the route already has a locale', async () => {
const requestOptions = {
followRedirect: false,
resolveWithFullResponse: true,
simple: false, // Don't reject on non-2xx response
headers: {
'Accept-Language': 'fr'
}
}
const response = await get('/en', requestOptions)
expect(response.statusCode).toBe(302)
expect(response.headers.location).toBe('/fr')
})

test('redirects subroute even if the route already has a locale', async () => {
const requestOptions = {
followRedirect: false,
resolveWithFullResponse: true,
simple: false, // Don't reject on non-2xx response
headers: {
'Accept-Language': 'fr'
}
}
const response = await get('/en/simple', requestOptions)
expect(response.statusCode).toBe(302)
expect(response.headers.location).toBe('/fr/simple')
})
})

describe('prefix + detectBrowserLanguage + onlyOnNoPrefix', () => {
/** @type {Nuxt} */
let nuxt

beforeAll(async () => {
const override = {
i18n: {
defaultLocale: 'fr',
strategy: 'prefix',
detectBrowserLanguage: {
useCookie: true,
onlyOnNoPrefix: true
}
}
}

nuxt = (await setup(loadConfig(__dirname, 'basic', override, { merge: true }))).nuxt
})

afterAll(async () => {
await nuxt.close()
})

test('does not redirect root if the route already has a locale', async () => {
const requestOptions = {
followRedirect: false,
resolveWithFullResponse: true,
simple: false, // Don't reject on non-2xx response
headers: {
'Accept-Language': 'fr'
}
}
const response = await get('/en', requestOptions)
expect(response.statusCode).toBe(200)
})

test('does not redirect subroute if the route already has a locale', async () => {
const requestOptions = {
followRedirect: false,
resolveWithFullResponse: true,
simple: false, // Don't reject on non-2xx response
headers: {
'Accept-Language': 'fr'
}
}
const response = await get('/en/simple', requestOptions)
expect(response.statusCode).toBe(200)
})
})

describe('prefix + detectBrowserLanguage + alwaysRedirect', () => {
/** @type {Nuxt} */
let nuxt
Expand Down Expand Up @@ -1539,6 +1651,20 @@ describe('prefix + detectBrowserLanguage + alwaysRedirect', () => {
const dom = getDom(html)
expect(dom.querySelector('#current-locale')?.textContent).toBe('locale: fr')
})

test('redirects although the route already has a locale', async () => {
const requestOptions = {
followRedirect: false,
resolveWithFullResponse: true,
simple: false, // Don't reject on non-2xx response
headers: {
'Accept-Language': 'fr'
}
}
const response = await get('/en', requestOptions)
expect(response.statusCode).toBe(302)
expect(response.headers.location).toBe('/fr')
})
})

describe('generate with detectBrowserLanguage.fallbackLocale', () => {
Expand Down
1 change: 1 addition & 0 deletions types/nuxt-i18n.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ declare namespace NuxtVueI18n {
cookieKey?: string
alwaysRedirect?: boolean
fallbackLocale?: Locale | null
onlyOnNoPrefix?: boolean
onlyOnRoot?: boolean
}

Expand Down