Skip to content

Commit

Permalink
feat: "onlyOnNoPrefix" - detect browser locale when no prefix (nuxt-m…
Browse files Browse the repository at this point in the history
…odules#896)

Co-authored-by: Rafał Chłodnicki <rchl2k@gmail.com>
  • Loading branch information
dword-design and rchl authored Dec 3, 2020
1 parent af42689 commit 15f0a44
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 7 deletions.
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

0 comments on commit 15f0a44

Please sign in to comment.