Skip to content

Commit

Permalink
fix: failure to change locale on initial try with nuxt generate
Browse files Browse the repository at this point in the history
When generating static files with `nuxt generate` (with default
`universal` mode), initial load of the URL doesn't trigger middleware.

Nuxt triggers middleware in those cases for initial load:

 - universal (server: yes, client: no)
 - spa (server: --, client: yes)
 - universal+generate (server: --, client: no)
 - spa+generate (server: --, client: yes)

Due to middleware not triggering on initial load, browser language
wasn't detected and cookie was not set, which later caused failure to
update locale on navigating to route from other locale. That's because
detection happened on second navigation and overridden target route's
locale.

To fix, trigger language detection from the plugin first.
For some cases that means that middleware will attempt to trigger
detection again but it will be short-cut early as cookie will already
be there or target locale will already be set.

Also cleaned up some imports and unused variables in middleware and
plugin (more cleanup is possible).

Resolves #378
  • Loading branch information
rchl committed Sep 5, 2019
1 parent 6879e6e commit 9b4b6f6
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 69 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ node_modules
.idea
*.log*
.nuxt
.nuxt-generate
.vscode
.DS_STORE
coverage
dist
dist
55 changes: 51 additions & 4 deletions src/plugins/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ const STRATEGIES = <%= JSON.stringify(options.STRATEGIES) %>
const STRATEGY = '<%= options.strategy %>'
const lazy = <%= options.lazy %>
const vuex = <%= JSON.stringify(options.vuex) %>
// Helpers
const getLocaleCodes = <%= options.getLocaleCodes %>
const localeCodes = getLocaleCodes(<%= JSON.stringify(options.locales) %>)

export default async (context) => {
const { app, route, store, req, res, redirect } = context;
const { app, route, store, req, res, redirect } = context

// Helpers
const getLocaleCodes = <%= options.getLocaleCodes %>
const getLocaleFromRoute = <%= options.getLocaleFromRoute %>
const getHostname = <%= options.getHostname %>
const getForwarded = <%= options.getForwarded %>
Expand Down Expand Up @@ -84,7 +86,7 @@ export default async (context) => {
const getLocaleCookie = () => {
if (useCookie) {
if (process.client) {
return JsCookie.get(cookieKey);
return JsCookie.get(cookieKey)
} else if (req && typeof req.headers.cookie !== 'undefined') {
const cookies = req.headers && req.headers.cookie ? Cookie.parse(req.headers.cookie) : {}
return cookies[cookieKey]
Expand All @@ -94,7 +96,7 @@ export default async (context) => {

const setLocaleCookie = locale => {
if (!useCookie) {
return;
return
}
const date = new Date()
if (process.client) {
Expand Down Expand Up @@ -210,4 +212,49 @@ export default async (context) => {
}

await loadAndSetLocale(locale, { initialSetup: true })

app.i18n.__detectBrowserLanguage = async route => {
const { alwaysRedirect, fallbackLocale } = detectBrowserLanguage

if (detectBrowserLanguage) {
let browserLocale

if (useCookie && (browserLocale = getLocaleCookie()) && browserLocale !== 1 && browserLocale !== '1') {
// Get preferred language from cookie if present and enabled
// Exclude 1 for backwards compatibility and fallback when fallbackLocale is empty
} else if (process.client && typeof navigator !== 'undefined' && navigator.language) {
// Get browser language either from navigator if running on client side, or from the headers
browserLocale = navigator.language.toLocaleLowerCase().substring(0, 2)
} else if (req && typeof req.headers['accept-language'] !== 'undefined') {
browserLocale = req.headers['accept-language'].split(',')[0].toLocaleLowerCase().substring(0, 2)
}

if (browserLocale) {
// Handle cookie option to prevent multiple redirections
if (!useCookie || alwaysRedirect || !getLocaleCookie()) {
let redirectToLocale = fallbackLocale

// Use browserLocale if we support it, otherwise use fallbackLocale
if (localeCodes.includes(browserLocale)) {
redirectToLocale = browserLocale
}

if (redirectToLocale && localeCodes.includes(redirectToLocale)) {
if (redirectToLocale !== app.i18n.locale) {
// We switch the locale before redirect to prevent loops
await app.i18n.setLocale(redirectToLocale)
} else if (useCookie && !getLocaleCookie()) {
app.i18n.setLocaleCookie(redirectToLocale)
}
}

return true
}
}
}

return false
}

await app.i18n.__detectBrowserLanguage(route)
}
66 changes: 10 additions & 56 deletions src/templates/middleware.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,15 @@
import Cookie from 'cookie'
import JsCookie from 'js-cookie'
import middleware from '../middleware'

middleware['i18n'] = async (context) => {
const { app, req, route, store, redirect, isHMR } = context;
const { app, req, route, redirect, isHMR } = context

if (isHMR) {
return
}

// Options
const STRATEGIES = <%= JSON.stringify(options.STRATEGIES) %>
const STRATEGY = '<%= options.strategy %>'
const lazy = <%= options.lazy %>
const vuex = <%= JSON.stringify(options.vuex) %>
const differentDomains = <%= options.differentDomains %>

// Helpers
const LOCALE_CODE_KEY = '<%= options.LOCALE_CODE_KEY %>'
const getLocaleCodes = <%= options.getLocaleCodes %>
const getLocaleFromRoute = <%= options.getLocaleFromRoute %>
const routesNameSeparator = '<%= options.routesNameSeparator %>'
const defaultLocaleRouteNameSuffix = '<%= options.defaultLocaleRouteNameSuffix %>'
const locales = getLocaleCodes(<%= JSON.stringify(options.locales) %>)
const syncVuex = <%= options.syncVuex %>

let locale = app.i18n.locale || app.i18n.defaultLocale || null

// Handle root path redirect
const rootRedirect = '<%= options.rootRedirect %>'
Expand All @@ -37,49 +21,19 @@ middleware['i18n'] = async (context) => {
// Update for setLocale to have up to date route
app.i18n.__route = route

// Handle browser language detection
const detectBrowserLanguage = <%= JSON.stringify(options.detectBrowserLanguage) %>
const routeLocale = getLocaleFromRoute(route, routesNameSeparator, defaultLocaleRouteNameSuffix, locales)

const { useCookie, cookieKey, alwaysRedirect, fallbackLocale } = detectBrowserLanguage
const { getLocaleCookie } = app.i18n

if (detectBrowserLanguage) {
let browserLocale

if (useCookie && (browserLocale = getLocaleCookie()) && browserLocale !== 1 && browserLocale !== '1') {
// Get preferred language from cookie if present and enabled
// Exclude 1 for backwards compatibility and fallback when fallbackLocale is empty
} else if (process.client && typeof navigator !== 'undefined' && navigator.language) {
// Get browser language either from navigator if running on client side, or from the headers
browserLocale = navigator.language.toLocaleLowerCase().substring(0, 2)
} else if (req && typeof req.headers['accept-language'] !== 'undefined') {
browserLocale = req.headers['accept-language'].split(',')[0].toLocaleLowerCase().substring(0, 2)
}

if (browserLocale) {
// Handle cookie option to prevent multiple redirections
if (!useCookie || alwaysRedirect || !getLocaleCookie()) {
let redirectToLocale = fallbackLocale

// Use browserLocale if we support it, otherwise use fallbackLocale
if (locales.includes(browserLocale)) {
redirectToLocale = browserLocale
}
if (detectBrowserLanguage && await app.i18n.__detectBrowserLanguage(route)) {
return
}

if (redirectToLocale && locales.includes(redirectToLocale)) {
if (redirectToLocale !== app.i18n.locale) {
// We switch the locale before redirect to prevent loops
await app.i18n.setLocale(redirectToLocale)
} else if (useCookie && !getLocaleCookie()) {
app.i18n.setLocaleCookie(redirectToLocale)
}
}
const locale = app.i18n.locale || app.i18n.defaultLocale || null
const getLocaleFromRoute = <%= options.getLocaleFromRoute %>
const routesNameSeparator = '<%= options.routesNameSeparator %>'
const defaultLocaleRouteNameSuffix = '<%= options.defaultLocaleRouteNameSuffix %>'
const locales = getLocaleCodes(<%= JSON.stringify(options.locales) %>)

return
}
}
}
const routeLocale = getLocaleFromRoute(route, routesNameSeparator, defaultLocaleRouteNameSuffix, locales)

await app.i18n.setLocale(routeLocale ? routeLocale : locale)
}
70 changes: 62 additions & 8 deletions test/browser.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import getPort from 'get-port'
import { resolve } from 'path'
import { createBrowser } from 'tib'
import { setup, loadConfig, url } from '@nuxtjs/module-test-utils'
import { generate, setup, loadConfig, url } from '@nuxtjs/module-test-utils'

const browserString = process.env.BROWSER_STRING || 'puppeteer/core'

const createNavigator = page => {
return async path => {
// When returning value resolved by `push`, `chrome/selenium`` crashes with:
// WebDriverError: unknown error: unhandled inspector error: {"code":-32000,"message":"Object reference chain is too long"}
// Chain and return nothing to work around.
await page.runAsyncScript(path => window.$nuxt.$router.push(path).then(() => {}), path)
await new Promise(resolve => setTimeout(resolve, 50))
}
}

describe(browserString, () => {
let nuxt
let browser
Expand All @@ -15,13 +27,7 @@ describe(browserString, () => {
staticServer: false,
extendPage (page) {
return {
async navigate (path) {
// When returning value resolved by `push`, `chrome/selenium`` crashes with:
// WebDriverError: unknown error: unhandled inspector error: {"code":-32000,"message":"Object reference chain is too long"}
// Chain and return nothing to work around.
await page.runAsyncScript(path => window.$nuxt.$router.push(path).then(() => {}), path)
await new Promise(resolve => setTimeout(resolve, 50))
}
navigate: createNavigator(page)
}
}
})
Expand Down Expand Up @@ -53,3 +59,51 @@ describe(browserString, () => {
expect(await page.getText('body')).toContain('page: À propos')
})
})

describe(`${browserString} (generate)`, () => {
let browser
let page
let port
// Local method that overrides imported one.
let url

beforeAll(async () => {
const distDir = resolve(__dirname, 'fixture', 'basic', '.nuxt-generate')

await generate(loadConfig(__dirname, 'basic', { generate: { dir: distDir } }))

port = await getPort()
url = path => `http://localhost:${port}${path}`

browser = await createBrowser(browserString, {
folder: distDir,
staticServer: {
folder: distDir,
port
},
extendPage (page) {
return {
navigate: createNavigator(page)
}
}
})
})

afterAll(async () => {
if (browser) {
await browser.close()
}
})

// Issue https://github.com/nuxt-community/nuxt-i18n/issues/378
test('navigate to non-default locale', async () => {
page = await browser.page(url('/'))
expect(await page.getText('body')).toContain('locale: en')

await page.navigate('/fr')
expect(await page.getText('body')).toContain('locale: fr')

await page.navigate('/')
expect(await page.getText('body')).toContain('locale: en')
})
})

0 comments on commit 9b4b6f6

Please sign in to comment.