Skip to content

Commit

Permalink
feat: Dynamic route parameters translation (#345)
Browse files Browse the repository at this point in the history
* feat: Dynamic route parameters translation

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 20, 2019
1 parent 741ae12 commit 2d1d729
Show file tree
Hide file tree
Showing 14 changed files with 176 additions and 92 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

# [6.0.0-0](https://github.com/nuxt-community/nuxt-i18n/compare/v5.12.8...v6.0.0-0) (2019-07-01)


### Features

* Dynamic route parameters translation ([04373ef](https://github.com/nuxt-community/nuxt-i18n/commit/04373ef)), closes [#79](https://github.com/nuxt-community/nuxt-i18n/issues/79)


### BREAKING CHANGES

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



## [5.12.8](https://github.com/nuxt-community/nuxt-i18n/compare/v5.12.6...v5.12.8) (2019-07-01)

> NOTE: Version bump only, all fixes were released in `v5.12.7` already
Expand Down
30 changes: 30 additions & 0 deletions docs/lang-switcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,33 @@ 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 ({ store }) {
await store.dispatch('i18n/setRouteParams', {
en: { slug: 'my-post' },
fr: { slug: 'mon-article' }
})
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 @@ -100,11 +100,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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nuxt-i18n",
"version": "5.12.8",
"version": "6.0.0-0",
"description": "i18n for Nuxt",
"license": "MIT",
"contributors": [
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
18 changes: 16 additions & 2 deletions src/plugins/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import JsCookie from 'js-cookie'
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import { nuxtI18nSeo } from './seo-head'
import { validateRouteParams } from './utils'

Vue.use(VueI18n)

Expand Down Expand Up @@ -30,14 +31,21 @@ 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) {
if (process.env.NODE_ENV === 'development') {
validateRouteParams(params)
}
commit(vuex.mutations.setRouteParams, params)
}
},
mutations: {
Expand All @@ -46,9 +54,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
26 changes: 26 additions & 0 deletions src/templates/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
const LOCALE_CODE_KEY = '<%= options.LOCALE_CODE_KEY %>'
const LOCALE_DOMAIN_KEY = '<%= options.LOCALE_DOMAIN_KEY %>'
const getLocaleCodes = <%= options.getLocaleCodes %>
const locales = <%= JSON.stringify(options.locales) %>
const localeCodes = getLocaleCodes(locales)

const isObject = value => value && !Array.isArray(value) && typeof value === 'object'

/**
* Asynchronously load messages from translation files
* @param {VueI18n} i18n vue-i18n instance
Expand Down Expand Up @@ -37,3 +45,21 @@ export async function loadLanguageAsync (context, locale) {
}
return Promise.resolve()
}

/**
* Validate setRouteParams action's payload
* @param {*} routeParams The action's payload
*/
export const validateRouteParams = routeParams => {
if (!isObject(routeParams)) {
console.warn(`[<%= options.MODULE_NAME %>] Route params should be an object`)
return
}
Object.entries(routeParams).forEach(([key, value]) => {
if (!localeCodes.includes(key)) {
console.warn(`[<%= options.MODULE_NAME %>] Trying to set route params for key ${key} which is not a valid locale`)
} else if (!isObject(value)) {
console.warn(`[<%= options.MODULE_NAME %>] Trying to set route params for locale ${key} with a non-object value`)
}
})
}
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;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 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;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 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;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 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;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 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;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>
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 () => {
const 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 () => {
const 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 () => {
const 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 () => {
const 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>
9 changes: 8 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,13 @@ import LangSwitcher from '../../components/LangSwitcher'
export default {
components: {
LangSwitcher
},
async asyncData ({ store }) {
await store.dispatch('i18n/setRouteParams', {
en: { slug: 'my-post' },
fr: { slug: 'mon-article' }
})
return {}
}
}
</script>

0 comments on commit 2d1d729

Please sign in to comment.