Skip to content

Commit

Permalink
feat: Dynamic route parameters translation
Browse files Browse the repository at this point in the history
Adds support for translating dynamic route parameters via the Vuex store module

BREAKING CHANGE: `preserveState` is now set automatically when registering the store module and
cannot be set via the configuration anymore

close #79
  • Loading branch information
paulgv committed Jul 1, 2019
1 parent a891f55 commit 04373ef
Show file tree
Hide file tree
Showing 11 changed files with 139 additions and 91 deletions.
33 changes: 33 additions & 0 deletions docs/lang-switcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,36 @@ computed: {
```

If `detectBrowserLanguage.useCookie` and `detectBrowserLanguage.alwaysRedirect` options are enabled, you might want to persist change to locale by calling `this.$i18n.setLocaleCookie(locale)` (or `app.i18n.setLocaleCookie(locale)`) method. Otherwise locale will switch back to saved one during navigation.

## Dynamic route parameters

Dealing with dynamic route parameters requires a bit more work because you need to provide parameters translations to **nuxt-i18n**. For this purpose, **nuxt-i18n**'s store module exposes a `routeParams` state property that will be merged with route params when generating lang switch routes with `switchLocalePath()`.

> NOTE: Make sure that Vuex [is enabled](https://nuxtjs.org/guide/vuex-store) in your app and that you did not set `vuex` option to `false` in **nuxt-i18n**'s options.
To provide dynamic parameters translations, dispatch the `i18n/setRouteParams` as early as possible when loading a page, eg:

```vue
<template>
<!-- pages/_slug.vue -->
</template>
<script>
export default {
async asyncData ({ app, store }) {
const params = await new Promise(resolve => {
resolve({
en: { slug: 'my-post' },
fr: { slug: 'mon-article' },
})
})
store.dispatch('i18n/setRouteParams', params)
return {
// your data
}
}
}
</script>
```

> NOTE: **nuxt-i18n** won't reset parameters translations for you, this means that if you use identical parameters for different routes, navigating between those routes might result in conflicting parameters. Make sure you always set params translations in such cases.
8 changes: 4 additions & 4 deletions docs/options-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@ Here are all the options available when configuring the module and their default
setLocale: 'I18N_SET_LOCALE',

// Mutation to commit to store current message, set to false to disable
setMessages: 'I18N_SET_MESSAGES'
},
setMessages: 'I18N_SET_MESSAGES',

// PreserveState from server
preserveState: false
// Mutation to commit to set route parameters translations
setRouteParams: 'I18N_SET_ROUTE_PARAMS'
}
},

// By default, custom routes are extracted from page files using acorn parsing,
Expand Down
6 changes: 3 additions & 3 deletions src/helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ exports.DEFAULT_OPTIONS = {
moduleName: 'i18n',
mutations: {
setLocale: 'I18N_SET_LOCALE',
setMessages: 'I18N_SET_MESSAGES'
},
preserveState: false
setMessages: 'I18N_SET_MESSAGES',
setRouteParams: 'I18N_SET_ROUTE_PARAMS'
}
},
parsePages: true,
pages: {},
Expand Down
14 changes: 12 additions & 2 deletions src/plugins/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,18 @@ export default async (context) => {
namespaced: true,
state: () => ({
locale: '',
messages: {}
messages: {},
routeParams: {}
}),
actions: {
setLocale ({ commit }, locale) {
commit(vuex.mutations.setLocale, locale)
},
setMessages ({ commit }, messages) {
commit(vuex.mutations.setMessages, messages)
},
setRouteParams ({ commit }, params) {
commit(vuex.mutations.setRouteParams, params)
}
},
mutations: {
Expand All @@ -46,9 +50,15 @@ export default async (context) => {
},
[vuex.mutations.setMessages] (state, messages) {
state.messages = messages
},
[vuex.mutations.setRouteParams] (state, params) {
state.routeParams = params
}
},
getters: {
localeRouteParams: ({ routeParams }) => locale => routeParams[locale] || {}
}
}, { preserveState: vuex.preserveState })
}, { preserveState: !!store.state[vuex.moduleName] })
}
<% } %>

Expand Down
13 changes: 12 additions & 1 deletion src/plugins/routing.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import './middleware'
import Vue from 'vue'

const vuex = <%= JSON.stringify(options.vuex) %>
const routesNameSeparator = '<%= options.routesNameSeparator %>'

function localePathFactory (i18nPath, routerPath) {
Expand Down Expand Up @@ -62,9 +63,19 @@ function switchLocalePathFactory (i18nPath) {
}

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

Expand Down
60 changes: 0 additions & 60 deletions test/fixtures/basic/__snapshots__/module.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -105,66 +105,6 @@ exports[`basic /fr/notlocalized contains FR text 1`] = `
"
`;
exports[`basic /fr/posts contains FR text, link to /posts/ & link to /fr/posts/my-slug 1`] = `
"<!doctype html>
<html data-n-head-ssr lang=\\"fr-FR\\" data-n-head=\\"lang\\">
<head data-n-head=\\"\\">
<meta data-n-head=\\"true\\" data-hid=\\"og:locale\\" property=\\"og:locale\\" content=\\"fr_FR\\"><meta data-n-head=\\"true\\" data-hid=\\"og:locale:alternate-en-US\\" property=\\"og:locale:alternate\\" content=\\"en_US\\"><link data-n-head=\\"true\\" data-hid=\\"alternate-hreflang-en-US\\" rel=\\"alternate\\" href=\\"nuxt-app.localhost/posts/\\" hreflang=\\"en-US\\"><link data-n-head=\\"true\\" data-hid=\\"alternate-hreflang-fr-FR\\" rel=\\"alternate\\" href=\\"nuxt-app.localhost/fr/posts/\\" hreflang=\\"fr-FR\\"><style data-vue-ssr-id=\\"2eed86ac:0\\">.nuxt-progress{position:fixed;top:0;left:0;right:0;height:2px;width:0;opacity:1;-webkit-transition:width .1s,opacity .4s;transition:width .1s,opacity .4s;background-color:#000;z-index:999999}.nuxt-progress.nuxt-progress-notransition{-webkit-transition:none;transition:none}.nuxt-progress-failed{background-color:red}</style>
</head>
<body data-n-head=\\"\\">
<div data-server-rendered=\\"true\\" id=\\"__nuxt\\"><!----><div id=\\"__layout\\"><div><div><a href=\\"/posts/\\">English</a><!----></div>
Articles
<div><a href=\\"/fr/posts/my-slug\\">my-slug</a></div></div></div></div>
</body>
</html>
"
`;
exports[`basic /fr/posts/my-slug contains FR text, post's slug, link to /posts/my-slug & link to /fr/posts/ 1`] = `
"<!doctype html>
<html data-n-head-ssr lang=\\"fr-FR\\" data-n-head=\\"lang\\">
<head data-n-head=\\"\\">
<meta data-n-head=\\"true\\" data-hid=\\"og:locale\\" property=\\"og:locale\\" content=\\"fr_FR\\"><meta data-n-head=\\"true\\" data-hid=\\"og:locale:alternate-en-US\\" property=\\"og:locale:alternate\\" content=\\"en_US\\"><link data-n-head=\\"true\\" data-hid=\\"alternate-hreflang-en-US\\" rel=\\"alternate\\" href=\\"nuxt-app.localhost/posts/my-slug\\" hreflang=\\"en-US\\"><link data-n-head=\\"true\\" data-hid=\\"alternate-hreflang-fr-FR\\" rel=\\"alternate\\" href=\\"nuxt-app.localhost/fr/posts/my-slug\\" hreflang=\\"fr-FR\\"><style data-vue-ssr-id=\\"2eed86ac:0\\">.nuxt-progress{position:fixed;top:0;left:0;right:0;height:2px;width:0;opacity:1;-webkit-transition:width .1s,opacity .4s;transition:width .1s,opacity .4s;background-color:#000;z-index:999999}.nuxt-progress.nuxt-progress-notransition{-webkit-transition:none;transition:none}.nuxt-progress-failed{background-color:red}</style>
</head>
<body data-n-head=\\"\\">
<div data-server-rendered=\\"true\\" id=\\"__nuxt\\"><!----><div id=\\"__layout\\"><div><div><a href=\\"/posts/my-slug\\">English</a><!----></div>
Articles
<div><h1>my-slug</h1> <a href=\\"/fr/posts/\\">index</a></div></div></div></div>
</body>
</html>
"
`;
exports[`basic /posts contains EN text, link to /fr/posts/ & link to /posts/my-slug 1`] = `
"<!doctype html>
<html data-n-head-ssr lang=\\"en-US\\" data-n-head=\\"lang\\">
<head data-n-head=\\"\\">
<meta data-n-head=\\"true\\" data-hid=\\"og:locale\\" property=\\"og:locale\\" content=\\"en_US\\"><meta data-n-head=\\"true\\" data-hid=\\"og:locale:alternate-fr-FR\\" property=\\"og:locale:alternate\\" content=\\"fr_FR\\"><link data-n-head=\\"true\\" data-hid=\\"alternate-hreflang-en-US\\" rel=\\"alternate\\" href=\\"nuxt-app.localhost/posts/\\" hreflang=\\"en-US\\"><link data-n-head=\\"true\\" data-hid=\\"alternate-hreflang-fr-FR\\" rel=\\"alternate\\" href=\\"nuxt-app.localhost/fr/posts/\\" hreflang=\\"fr-FR\\"><style data-vue-ssr-id=\\"2eed86ac:0\\">.nuxt-progress{position:fixed;top:0;left:0;right:0;height:2px;width:0;opacity:1;-webkit-transition:width .1s,opacity .4s;transition:width .1s,opacity .4s;background-color:#000;z-index:999999}.nuxt-progress.nuxt-progress-notransition{-webkit-transition:none;transition:none}.nuxt-progress-failed{background-color:red}</style>
</head>
<body data-n-head=\\"\\">
<div data-server-rendered=\\"true\\" id=\\"__nuxt\\"><!----><div id=\\"__layout\\"><div><div><!----><a href=\\"/fr/posts/\\">Français</a></div>
Posts
<div><a href=\\"/posts/my-slug\\">my-slug</a></div></div></div></div>
</body>
</html>
"
`;
exports[`basic /posts/my-slug contains EN text, post's slug, link to /fr/posts/my-slug & link to /posts/ 1`] = `
"<!doctype html>
<html data-n-head-ssr lang=\\"en-US\\" data-n-head=\\"lang\\">
<head data-n-head=\\"\\">
<meta data-n-head=\\"true\\" data-hid=\\"og:locale\\" property=\\"og:locale\\" content=\\"en_US\\"><meta data-n-head=\\"true\\" data-hid=\\"og:locale:alternate-fr-FR\\" property=\\"og:locale:alternate\\" content=\\"fr_FR\\"><link data-n-head=\\"true\\" data-hid=\\"alternate-hreflang-en-US\\" rel=\\"alternate\\" href=\\"nuxt-app.localhost/posts/my-slug\\" hreflang=\\"en-US\\"><link data-n-head=\\"true\\" data-hid=\\"alternate-hreflang-fr-FR\\" rel=\\"alternate\\" href=\\"nuxt-app.localhost/fr/posts/my-slug\\" hreflang=\\"fr-FR\\"><style data-vue-ssr-id=\\"2eed86ac:0\\">.nuxt-progress{position:fixed;top:0;left:0;right:0;height:2px;width:0;opacity:1;-webkit-transition:width .1s,opacity .4s;transition:width .1s,opacity .4s;background-color:#000;z-index:999999}.nuxt-progress.nuxt-progress-notransition{-webkit-transition:none;transition:none}.nuxt-progress-failed{background-color:red}</style>
</head>
<body data-n-head=\\"\\">
<div data-server-rendered=\\"true\\" id=\\"__nuxt\\"><!----><div id=\\"__layout\\"><div><div><!----><a href=\\"/fr/posts/my-slug\\">Français</a></div>
Posts
<div><h1>my-slug</h1> <a href=\\"/posts/\\">index</a></div></div></div></div>
</body>
</html>
"
`;
exports[`basic sets SEO metadata properly 1`] = `
"
<meta data-n-head=\\"true\\" data-hid=\\"og:locale\\" property=\\"og:locale\\" content=\\"en_US\\"><meta data-n-head=\\"true\\" data-hid=\\"og:locale:alternate-fr-FR\\" property=\\"og:locale:alternate\\" content=\\"fr_FR\\"><link data-n-head=\\"true\\" data-hid=\\"alternate-hreflang-en-US\\" rel=\\"alternate\\" href=\\"nuxt-app.localhost/\\" hreflang=\\"en-US\\"><link data-n-head=\\"true\\" data-hid=\\"alternate-hreflang-fr-FR\\" rel=\\"alternate\\" href=\\"nuxt-app.localhost/fr\\" hreflang=\\"fr-FR\\"><style data-vue-ssr-id=\\"2eed86ac:0\\">.nuxt-progress{position:fixed;top:0;left:0;right:0;height:2px;width:0;opacity:1;-webkit-transition:width .1s,opacity .4s;transition:width .1s,opacity .4s;background-color:#000;z-index:999999}.nuxt-progress.nuxt-progress-notransition{-webkit-transition:none;transition:none}.nuxt-progress-failed{background-color:red}</style>
Expand Down
62 changes: 45 additions & 17 deletions test/fixtures/basic/module.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ process.env.NODE_ENV = 'production'

const { Nuxt, Builder } = require('nuxt')
const request = require('request-promise-native')
const { JSDOM } = require('jsdom')

const config = require('./nuxt.config')

const { cleanUpScripts } = require('../../utils')

const url = path => `http://localhost:${process.env.PORT}${path}`
const get = path => request(url(path))
const getDom = html => (new JSDOM(html)).window.document

describe('basic', () => {
let nuxt
Expand Down Expand Up @@ -74,24 +76,50 @@ describe('basic', () => {
expect(response.statusCode).toBe(404)
})

test('/posts contains EN text, link to /fr/posts/ & link to /posts/my-slug', async () => {
let html = await get('/posts')
expect(cleanUpScripts(html)).toMatchSnapshot()
})

test('/posts/my-slug contains EN text, post\'s slug, link to /fr/posts/my-slug & link to /posts/', async () => {
let html = await get('/posts/my-slug')
expect(cleanUpScripts(html)).toMatchSnapshot()
})

test('/fr/posts contains FR text, link to /posts/ & link to /fr/posts/my-slug', async () => {
let html = await get('/fr/posts')
expect(cleanUpScripts(html)).toMatchSnapshot()
})
describe('posts', () => {
let html
let title
let langSwitcherLink
let link
const getElements = () => {
const dom = getDom(html)
title = dom.querySelector('h1')
const links = [...dom.querySelectorAll('a')]
langSwitcherLink = links[0]
link = links[1]
}

test('/fr/posts/my-slug contains FR text, post\'s slug, link to /posts/my-slug & link to /fr/posts/', async () => {
let html = await get('/fr/posts/my-slug')
expect(cleanUpScripts(html)).toMatchSnapshot()
test('/posts contains EN text, link to /fr/articles/ & link to /posts/my-post', async () => {
html = await get('/posts')
getElements()
expect(title.textContent).toBe('Posts')
expect(langSwitcherLink.href).toBe('/fr/articles/')
expect(link.href).toBe('/posts/my-post')
})

test('/posts/my-post contains EN text, link to /fr/articles/mon-article & link to /posts/', async () => {
html = await get('/posts/my-post')
getElements()
expect(title.textContent).toBe('Posts')
expect(langSwitcherLink.href).toBe('/fr/articles/mon-article')
expect(link.href).toBe('/posts/')
})

test('/fr/articles contains FR text, link to /posts/ & link to /fr/articles/mon-article', async () => {
html = await get('/fr/articles')
getElements()
expect(title.textContent).toBe('Articles')
expect(langSwitcherLink.href).toBe('/posts/')
expect(link.href).toBe('/fr/articles/mon-article')
})

test('/fr/articles/mon-article contains FR text, link to /posts/my-post & link to /fr/articles/', async () => {
html = await get('/fr/articles/mon-article')
getElements()
expect(title.textContent).toBe('Articles')
expect(langSwitcherLink.href).toBe('/posts/my-post')
expect(link.href).toBe('/fr/articles/')
})
})

test('/dynamicNested/1/2/3 contains link to /fr/imbrication-dynamique/1/2/3', async () => {
Expand Down
7 changes: 6 additions & 1 deletion test/fixtures/basic/pages/posts.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div>
<LangSwitcher />
{{ $t('posts') }}
<h1>{{ $t('posts') }}</h1>
<router-view></router-view>
</div>
</template>
Expand All @@ -12,6 +12,11 @@ import LangSwitcher from '../components/LangSwitcher'
export default {
components: {
LangSwitcher
},
nuxtI18n: {
paths: {
fr: '/articles'
}
}
}
</script>
12 changes: 11 additions & 1 deletion test/fixtures/basic/pages/posts/_slug.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div>
<h1>{{ $route.params.slug }}</h1>
<h2>{{ $route.params.slug }}</h2>
<nuxt-link
exact
:to="localePath('posts')">index</nuxt-link>
Expand All @@ -13,6 +13,16 @@ import LangSwitcher from '../../components/LangSwitcher'
export default {
components: {
LangSwitcher
},
async asyncData ({ app, store }) {
const params = await new Promise(resolve => {
resolve({
en: { slug: 'my-post' },
fr: { slug: 'mon-article' },
})
})
store.dispatch('i18n/setRouteParams', params)
return {}
}
}
</script>
15 changes: 13 additions & 2 deletions test/fixtures/basic/pages/posts/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
:to="localePath({
name: 'posts-slug',
params: {
slug: 'my-slug'
slug: params[$i18n.locale].slug
}
})">my-slug</nuxt-link>
})">{{ params[$i18n.locale].slug }}</nuxt-link>
</div>
</template>

Expand All @@ -17,6 +17,17 @@ import LangSwitcher from '../../components/LangSwitcher'
export default {
components: {
LangSwitcher
},
async asyncData () {
const params = await new Promise(resolve => {
resolve({
en: { slug: 'my-post' },
fr: { slug: 'mon-article' },
})
})
return {
params
}
}
}
</script>
Empty file.

0 comments on commit 04373ef

Please sign in to comment.