Skip to content

Commit

Permalink
fix: make switchLocalePath work from Nuxt plugin or middleware
Browse files Browse the repository at this point in the history
switchLocalePath didn't work from a Nuxt plugin or middleware
(from context.app) as it accessed properties on `this` that were not
accessible from there.

Fixed by creating a proxy interface that is consistent for both
Vue instances and Nuxt context so that switchLocalePath can access
same properties from it and get expected results.

Also:
  - Updated `differentDomains` tests to not use snapshot testing.
    Those actually were wrongly tested due to bug in test utils but
    failure wasn't caught in snapshot (it was supposed to have
    SEO disabled).

Resolves #480
  • Loading branch information
rchl committed Oct 24, 2019
1 parent 971e5d2 commit 8a1c052
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 131 deletions.
220 changes: 122 additions & 98 deletions src/plugins/routing.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,148 +12,172 @@ import {
vuex
} from './options'

function localePathFactory (i18nPath, routerPath) {
return function localePath (route, locale) {
// Abort if no route or no locale
if (!route) return
function localePath (route, locale) {
// Abort if no route or no locale
if (!route) return

const i18n = this[i18nPath]
const { i18n } = this

if (strategy === STRATEGIES.NO_PREFIX && locale && locale !== i18n.locale) {
// eslint-disable-next-line no-console
console.warn(`[${MODULE_NAME}] Passing non-current locale to localePath is unsupported when using no_prefix strategy`)
}
if (strategy === STRATEGIES.NO_PREFIX && locale && locale !== i18n.locale) {
// eslint-disable-next-line no-console
console.warn(`[${MODULE_NAME}] Passing non-current locale to localePath is unsupported when using no_prefix strategy`)
}

locale = locale || i18n.locale
locale = locale || i18n.locale

if (!locale) return
if (!locale) return

// If route parameters is a string, use it as the route's name
if (typeof route === 'string') {
route = { name: route }
}
// If route parameters is a string, use it as the route's name
if (typeof route === 'string') {
route = { name: route }
}

const localizedRoute = Object.assign({}, route)
const localizedRoute = Object.assign({}, route)

if (route.path && !route.name) {
// if route has a path defined but no name, resolve full route using the path
const isPrefixed = (
// don't prefix default locale
!(locale === defaultLocale && strategy === STRATEGIES.PREFIX_EXCEPT_DEFAULT) &&
if (route.path && !route.name) {
// if route has a path defined but no name, resolve full route using the path
const isPrefixed = (
// don't prefix default locale
!(locale === defaultLocale && strategy === STRATEGIES.PREFIX_EXCEPT_DEFAULT) &&
// no prefix for any language
!(strategy === STRATEGIES.NO_PREFIX) &&
// no prefix for different domains
!i18n.differentDomains
)
)

const path = (isPrefixed ? `/${locale}${route.path}` : route.path)
const path = (isPrefixed ? `/${locale}${route.path}` : route.path)

localizedRoute.path = path
} else {
// otherwise resolve route via the route name
// Build localized route options
let name = route.name + (strategy === STRATEGIES.NO_PREFIX ? '' : routesNameSeparator + locale)
localizedRoute.path = path
} else {
// otherwise resolve route via the route name
// Build localized route options
let name = route.name + (strategy === STRATEGIES.NO_PREFIX ? '' : routesNameSeparator + locale)

// Match route without prefix for default locale
if (locale === defaultLocale && strategy === STRATEGIES.PREFIX_AND_DEFAULT) {
name += routesNameSeparator + defaultLocaleRouteNameSuffix
}
// Match route without prefix for default locale
if (locale === defaultLocale && strategy === STRATEGIES.PREFIX_AND_DEFAULT) {
name += routesNameSeparator + defaultLocaleRouteNameSuffix
}

localizedRoute.name = name
localizedRoute.name = name

const { params } = localizedRoute
if (params && params['0'] === undefined && params.pathMatch) {
params['0'] = params.pathMatch
}
const { params } = localizedRoute
if (params && params['0'] === undefined && params.pathMatch) {
params['0'] = params.pathMatch
}

// Resolve localized route
const router = this[routerPath]
const { route: { fullPath } } = router.resolve(localizedRoute)
return fullPath
}

// Resolve localized route
const { route: { fullPath } } = this.router.resolve(localizedRoute)
return fullPath
}

function switchLocalePathFactory (i18nPath) {
return function switchLocalePath (locale) {
if (strategy === STRATEGIES.NO_PREFIX && locale && locale !== this[i18nPath].locale) {
// eslint-disable-next-line no-console
console.warn(`[${MODULE_NAME}] Passing non-current locale to switchLocalePath is unsupported when using no_prefix strategy`)
}
function switchLocalePath (locale) {
const i18n = this.i18n

const name = this.getRouteBaseName()
if (!name) {
return ''
}
if (strategy === STRATEGIES.NO_PREFIX && locale && locale !== i18n.locale) {
// eslint-disable-next-line no-console
console.warn(`[${MODULE_NAME}] Passing non-current locale to switchLocalePath is unsupported when using no_prefix strategy`)
}

const name = this.getRouteBaseName()
if (!name) {
return ''
}

const { params, ...routeCopy } = this.$route
let langSwitchParams = {}
if (vuex && this.$store) {
langSwitchParams = this.$store.getters[`${vuex.moduleName}/localeRouteParams`](locale)
const { route, store } = this
const { params, ...routeCopy } = route
let langSwitchParams = {}
if (vuex && store) {
langSwitchParams = store.getters[`${vuex.moduleName}/localeRouteParams`](locale)
}
const baseRoute = Object.assign({}, routeCopy, {
name,
params: {
...params,
...langSwitchParams,
0: params.pathMatch
}
const baseRoute = Object.assign({}, routeCopy, {
name,
params: {
...params,
...langSwitchParams,
0: params.pathMatch
}
})
let path = this.localePath(baseRoute, locale)

// Handle different domains
if (this[i18nPath].differentDomains) {
const lang = this[i18nPath].locales.find(l => l[LOCALE_CODE_KEY] === locale)
if (lang && lang[LOCALE_DOMAIN_KEY]) {
let protocol
if (process.server) {
const isHTTPS = require('is-https')
const { req } = this.$options._parentVnode.ssrContext
protocol = isHTTPS(req) ? 'https' : 'http'
} else {
protocol = window.location.protocol.split(':')[0]
}
path = protocol + '://' + lang[LOCALE_DOMAIN_KEY] + path
})
let path = this.localePath(baseRoute, locale)

// Handle different domains
if (i18n.differentDomains) {
const lang = i18n.locales.find(l => l[LOCALE_CODE_KEY] === locale)
if (lang && lang[LOCALE_DOMAIN_KEY]) {
let protocol
if (process.server) {
const isHTTPS = require('is-https')
protocol = isHTTPS(this.req) ? 'https' : 'http'
} else {
// eslint-disable-next-line no-console
console.warn(`[${MODULE_NAME}] Could not find domain name for locale ${locale}`)
protocol = window.location.protocol.split(':')[0]
}
path = protocol + '://' + lang[LOCALE_DOMAIN_KEY] + path
} else {
// eslint-disable-next-line no-console
console.warn(`[${MODULE_NAME}] Could not find domain name for locale ${locale}`)
}
return path
}
return path
}

function getRouteBaseNameFactory (contextRoute) {
function givenOrCurrent (route) {
return route || this.$route
function getRouteBaseName () {
const { route } = this
if (!route.name) {
return null
}
return route.name.split(routesNameSeparator)[0]
}

const routeGetter = contextRoute ? route => route || contextRoute : givenOrCurrent
const VueInstanceProxy = function (targetFunction) {
return function () {
const proxy = {
getRouteBaseName: this.getRouteBaseName,
i18n: this.$i18n,
localePath: this.localePath,
req: process.server ? this.$ssrContext.req : null,
route: this.$route,
router: this.$router,
store: this.$store
}

return function getRouteBaseName (route) {
route = routeGetter.call(this, route)
if (!route.name) {
return null
return targetFunction.apply(proxy, arguments)
}
}

const NuxtContextProxy = function (context, targetFunction) {
return function () {
const { app, req, route, store } = context

const proxy = {
getRouteBaseName: app.getRouteBaseName,
i18n: app.i18n,
localePath: app.localePath,
req: process.server ? req : null,
route,
router: app.router,
store
}
return route.name.split(routesNameSeparator)[0]

return targetFunction.apply(proxy, arguments)
}
}

const plugin = {
install (Vue) {
Vue.mixin({
methods: {
localePath: localePathFactory('$i18n', '$router'),
switchLocalePath: switchLocalePathFactory('$i18n'),
getRouteBaseName: getRouteBaseNameFactory()
localePath: VueInstanceProxy(localePath),
switchLocalePath: VueInstanceProxy(switchLocalePath),
getRouteBaseName: VueInstanceProxy(getRouteBaseName)
}
})
}
}

export default ({ app, route }) => {
export default (context) => {
Vue.use(plugin)
app.localePath = localePathFactory('i18n', 'router')
app.switchLocalePath = switchLocalePathFactory('i18n')
app.getRouteBaseName = getRouteBaseNameFactory(route)
const { app } = context
app.localePath = NuxtContextProxy(context, localePath)
app.switchLocalePath = NuxtContextProxy(context, switchLocalePath)
app.getRouteBaseName = NuxtContextProxy(context, getRouteBaseName)
}
30 changes: 0 additions & 30 deletions test/__snapshots__/module.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -111,36 +111,6 @@ exports[`basic sets SEO metadata properly 1`] = `
"
`;
exports[`differentDomains enabled matches domain's locale (en) 1`] = `
"<!doctype html>
<html data-n-head-ssr lang=\\"en-US\\" data-n-head=\\"%7B%22lang%22:%7B%22ssr%22:%22en-US%22%7D%7D\\">
<head >
<meta data-n-head=\\"ssr\\" data-hid=\\"og:locale\\" property=\\"og:locale\\" content=\\"en_US\\"><meta data-n-head=\\"ssr\\" data-hid=\\"og:locale:alternate-fr-FR\\" property=\\"og:locale:alternate\\" content=\\"fr_FR\\"><link data-n-head=\\"ssr\\" data-hid=\\"alternate-hreflang-en-US\\" rel=\\"alternate\\" href=\\"nuxt-app.localhosthttp://en.nuxt-app.localhost/\\" hreflang=\\"en-US\\"><link data-n-head=\\"ssr\\" data-hid=\\"alternate-hreflang-fr-FR\\" rel=\\"alternate\\" href=\\"nuxt-app.localhosthttp://fr.nuxt-app.localhost/\\" hreflang=\\"fr-FR\\"><style data-vue-ssr-id=\\"4d2c2316:0\\">.nuxt-progress{position:fixed;top:0;left:0;right:0;height:2px;width:0;opacity:1;transition:width .1s,opacity .4s;background-color:#000;z-index:999999}.nuxt-progress.nuxt-progress-notransition{transition:none}.nuxt-progress-failed{background-color:red}</style>
</head>
<body >
<div data-server-rendered=\\"true\\" id=\\"__nuxt\\"><!----><div id=\\"__layout\\"><div><div><a href=\\"/http://fr.nuxt-app.localhost/\\">Français</a></div>
page: Homepage
<a href=\\"/about-us\\">About us</a> <div>locale: en</div></div></div></div>
</body>
</html>
"
`;
exports[`differentDomains enabled matches domain's locale (fr) 1`] = `
"<!doctype html>
<html data-n-head-ssr lang=\\"fr-FR\\" data-n-head=\\"%7B%22lang%22:%7B%22ssr%22:%22fr-FR%22%7D%7D\\">
<head >
<meta data-n-head=\\"ssr\\" data-hid=\\"og:locale\\" property=\\"og:locale\\" content=\\"fr_FR\\"><meta data-n-head=\\"ssr\\" data-hid=\\"og:locale:alternate-en-US\\" property=\\"og:locale:alternate\\" content=\\"en_US\\"><link data-n-head=\\"ssr\\" data-hid=\\"alternate-hreflang-en-US\\" rel=\\"alternate\\" href=\\"nuxt-app.localhosthttp://en.nuxt-app.localhost/\\" hreflang=\\"en-US\\"><link data-n-head=\\"ssr\\" data-hid=\\"alternate-hreflang-fr-FR\\" rel=\\"alternate\\" href=\\"nuxt-app.localhosthttp://fr.nuxt-app.localhost/\\" hreflang=\\"fr-FR\\"><style data-vue-ssr-id=\\"4d2c2316:0\\">.nuxt-progress{position:fixed;top:0;left:0;right:0;height:2px;width:0;opacity:1;transition:width .1s,opacity .4s;background-color:#000;z-index:999999}.nuxt-progress.nuxt-progress-notransition{transition:none}.nuxt-progress-failed{background-color:red}</style>
</head>
<body >
<div data-server-rendered=\\"true\\" id=\\"__nuxt\\"><!----><div id=\\"__layout\\"><div><div><a href=\\"/http://en.nuxt-app.localhost/\\">English</a></div>
page: Accueil
<a href=\\"/a-propos\\">À propos</a> <div>locale: fr</div></div></div></div>
</body>
</html>
"
`;
exports[`no_prefix strategy / contains EN text & link /about 1`] = `
"<!doctype html>
<html data-n-head-ssr lang=\\"en-US\\" data-n-head=\\"%7B%22lang%22:%7B%22ssr%22:%22en-US%22%7D%7D\\">
Expand Down
8 changes: 8 additions & 0 deletions test/browser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ describe(browserString, () => {

expect(await page.getText('body')).toContain('page: À propos')
})

test('APIs in app context work after SPA navigation', async () => {
page = await browser.page(url('/'))
await page.navigate('/middleware')

expect(await page.getText('#paths')).toBe('/middleware,/fr/middleware-fr')
expect(await page.getText('#name')).toBe('middleware')
})
})

describe(`${browserString} (generate)`, () => {
Expand Down
7 changes: 7 additions & 0 deletions test/fixture/basic/middleware/i18n-middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function ({ app, route }) {
const localeCodes = app.i18n.locales.map(locale => locale.code)

// Tests localePath, switchLocalePath and getRouteBaseName from app context.
app.allLocalePaths = localeCodes.map(locale => app.switchLocalePath(locale))
app.routeBaseName = app.getRouteBaseName()
}
30 changes: 30 additions & 0 deletions test/fixture/basic/pages/middleware.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<template>
<div>
<div id="paths">{{ allLocalePaths }}</div>
<div id="name">{{ routeBaseName }}</div>
</div>
</template>

<script>
export default {
middleware: ['i18n-middleware'],
nuxtI18n: {
paths: {
en: '/middleware',
fr: '/middleware-fr'
}
},
data() {
return {
allLocalePaths: [],
routeBaseName: '',
}
},
asyncData({ app }) {
return {
allLocalePaths: app.allLocalePaths.join(','),
routeBaseName: app.routeBaseName
}
}
}
</script>

0 comments on commit 8a1c052

Please sign in to comment.