Skip to content

Commit

Permalink
feat: Add NO_PREFIX strategy + setLocale function (#409)
Browse files Browse the repository at this point in the history
Also:

 - chore: remove constants.js from coverage
   The coverage was patching the default beforeLanguageSwitch & onLanguageSwitched
   null functions, but tests failed because the patched functions referenced
   unknown (out of scope?) variables.

   + I think that making coverage on a constant-declarations file is kinda
   pointless since all the lines are always covered by design.

 - fix(tests): don't use v-for and v-if together - warning fix

 - refactor: hoist constants outside of exported function

 - refactor: don't return messages from loadLanguageAsync

Co-authored-by: Thomas Reichling <[email protected]>
Co-authored-by: Rafał Chłodnicki <[email protected]>
  • Loading branch information
3 people committed Aug 27, 2019
1 parent 63ed8df commit 998011e
Show file tree
Hide file tree
Showing 22 changed files with 391 additions and 188 deletions.
1 change: 1 addition & 0 deletions docs/options-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Here are all the options available when configuring the module and their default
defaultLocaleRouteNameSuffix: 'default',

// Routes generation strategy, can be set to one of the following:
// - 'no_prefix': routes won't be prefixed
// - 'prefix_except_default': add locale prefix for every locale except default
// - 'prefix': add locale prefix for every locale
// - 'prefix_and_default': add locale prefix for every locale and default
Expand Down
12 changes: 9 additions & 3 deletions docs/routing.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Routing

**nuxt-i18n** overrides Nuxt default routes to add locale prefixes to every URL.
**nuxt-i18n** overrides Nuxt default routes to add locale prefixes to every URL (except in no_prefix strategy).
Say your app supports two languages: French and English as the default language, and you have the following pages in your project:

```asciidoc
Expand Down Expand Up @@ -40,7 +40,13 @@ Note that routes for the English version do not have any prefix because it is th
## Strategy
There are three supported strategies for generating the app's routes:
There are four supported strategies for generating the app's routes:
### no_prefix
With this strategy, your routes won't have a locale prefix added. The locale will be detected & changed without changing the URL. This implies that you have to rely on browser & cookie detection, and implement locale switches by calling the i18n API.
> NOTE: Currently this strategy doesn't support [Custom paths](#custom-paths).
### prefix_except_default
Expand All @@ -54,7 +60,7 @@ With this strategy, all routes will have a locale prefix.
This strategy combines both previous strategies behaviours, meaning that you will get URLs with prefixes for every language, but URLs for the default language will also have a non-prefixed version.
To configure the strategy, use the `strategy` option. Make sure you have a `defaultLocale` defined if using **prefix_except_default** or **prefix_and_default** strategy.
To configure the strategy, use the `strategy` option. Make sure you have a `defaultLocale` defined if using **prefix_except_default**, **prefix_and_default** or **no_prefix** strategy.
```js
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"src/**/*.js",
"!src/templates/*.js",
"!src/plugins/*.js",
"!src/helpers/utils.js"
"!src/helpers/utils.js",
"!src/helpers/constants.js"
]
},
"dependencies": {
Expand Down
3 changes: 2 additions & 1 deletion src/helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ exports.LOCALE_FILE_KEY = 'file'
const STRATEGIES = {
PREFIX: 'prefix',
PREFIX_EXCEPT_DEFAULT: 'prefix_except_default',
PREFIX_AND_DEFAULT: 'prefix_and_default'
PREFIX_AND_DEFAULT: 'prefix_and_default',
NO_PREFIX: 'no_prefix'
}

exports.STRATEGIES = STRATEGIES
Expand Down
34 changes: 23 additions & 11 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ module.exports = function (userOptions) {
}
}

if (!Object.values(STRATEGIES).includes(options.strategy)) {
// eslint-disable-next-line no-console
console.error('[' + options.MODULE_NAME + '] Invalid "strategy" option "' + options.strategy + '" (must be one of: ' + Object.values(STRATEGIES).join(', ') + ').')
return
}

const templatesOptions = {
...options,
MODULE_NAME,
Expand All @@ -58,19 +64,25 @@ module.exports = function (userOptions) {

// Generate localized routes
const pagesDir = this.options.dir && this.options.dir.pages ? this.options.dir.pages : 'pages'
this.extendRoutes((routes) => {
// This import (or more specifically 'vue-template-compiler' in helpers/components.js) needs to
// be required only at build time to avoid problems when 'vue-template-compiler' dependency is
// not available (at runtime, when using nuxt-start).
const { makeRoutes } = require('./helpers/routes')

const localizedRoutes = makeRoutes(routes, {
...options,
pagesDir
if (options.strategy !== STRATEGIES.NO_PREFIX) {
this.extendRoutes((routes) => {
// This import (or more specifically 'vue-template-compiler' in helpers/components.js) needs to
// be required only at build time to avoid problems when 'vue-template-compiler' dependency is
// not available (at runtime, when using nuxt-start).
const { makeRoutes } = require('./helpers/routes')

const localizedRoutes = makeRoutes(routes, {
...options,
pagesDir
})
routes.splice(0, routes.length)
routes.unshift(...localizedRoutes)
})
routes.splice(0, routes.length)
routes.unshift(...localizedRoutes)
})
} else if (options.differentDomains) {
// eslint-disable-next-line no-console
console.warn('[' + options.MODULE_NAME + '] The `differentDomains` option and `no_prefix` strategy are not compatible. Change strategy or disable `differentDomains` option.')
}

// Plugins
for (const file of requiredPlugins) {
Expand Down
97 changes: 71 additions & 26 deletions src/plugins/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ import { validateRouteParams } from './utils'

Vue.use(VueI18n)

export default async (context) => {
const { app, route, store, req, res } = context;
// Options
const LOCALE_CODE_KEY = '<%= options.LOCALE_CODE_KEY %>'
const LOCALE_DOMAIN_KEY = '<%= options.LOCALE_DOMAIN_KEY %>'
const STRATEGIES = <%= JSON.stringify(options.STRATEGIES) %>
const STRATEGY = '<%= options.strategy %>'
const lazy = <%= options.lazy %>
const vuex = <%= JSON.stringify(options.vuex) %>

// Options
const lazy = <%= options.lazy %>
const vuex = <%= JSON.stringify(options.vuex) %>
export default async (context) => {
const { app, route, store, req, res, redirect } = context;

// Helpers
const LOCALE_CODE_KEY = '<%= options.LOCALE_CODE_KEY %>'
const LOCALE_DOMAIN_KEY = '<%= options.LOCALE_DOMAIN_KEY %>'
const getLocaleCodes = <%= options.getLocaleCodes %>
const getLocaleFromRoute = <%= options.getLocaleFromRoute %>
const getHostname = <%= options.getHostname %>
Expand Down Expand Up @@ -79,6 +81,18 @@ export default async (context) => {
const detectBrowserLanguage = <%= JSON.stringify(options.detectBrowserLanguage) %>
const { useCookie, cookieKey } = detectBrowserLanguage

const getLocaleCookie = () => {
if (useCookie) {
if (process.client) {
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]
}
}
return null
}

const setLocaleCookie = locale => {
if (!useCookie) {
return;
Expand All @@ -105,6 +119,49 @@ export default async (context) => {
}
}

const loadAndSetLocale = async (newLocale, { initialSetup = false } = {}) => {
// Abort if different domains option enabled
if (app.i18n.differentDomains) {
return
}

// Abort if newLocale did not change
if (newLocale === app.i18n.locale) {
return
}

const oldLocale = app.i18n.locale
if (!initialSetup) {
app.i18n.beforeLanguageSwitch(oldLocale, newLocale)

if (useCookie) {
app.i18n.setLocaleCookie(newLocale)
}
}

// Lazy-loading enabled
if (lazy) {
const { loadLanguageAsync } = require('./utils')
await loadLanguageAsync(context, newLocale)
}

app.i18n.locale = newLocale

if (!initialSetup) {
app.i18n.onLanguageSwitched(oldLocale, newLocale)
}

await syncVuex(newLocale, app.i18n.getLocaleMessage(newLocale))

if (!initialSetup && STRATEGY !== STRATEGIES.NO_PREFIX) {
const routeName = route && route.name ? app.getRouteBaseName(route) : 'index'

redirect(app.localePath(Object.assign({}, route , {
name: routeName
}), newLocale))
}
}

// Set instance options
app.i18n = new VueI18n(<%= JSON.stringify(options.vueI18n) %>)
app.i18n.locales = <%= JSON.stringify(options.locales) %>
Expand All @@ -114,6 +171,9 @@ export default async (context) => {
app.i18n.beforeLanguageSwitch = <%= options.beforeLanguageSwitch %>
app.i18n.onLanguageSwitched = <%= options.onLanguageSwitched %>
app.i18n.setLocaleCookie = setLocaleCookie
app.i18n.getLocaleCookie = getLocaleCookie
app.i18n.setLocale = (locale) => loadAndSetLocale(locale)

// Extension of Vue
if (!app.$t) {
app.$t = app.i18n.t
Expand All @@ -133,30 +193,15 @@ export default async (context) => {
if (app.i18n.differentDomains) {
const domainLocale = getLocaleDomain()
locale = domainLocale ? domainLocale : locale
} else {
} else if (STRATEGY !== STRATEGIES.NO_PREFIX) {
const routesNameSeparator = '<%= options.routesNameSeparator %>'
const defaultLocaleRouteNameSuffix = '<%= options.defaultLocaleRouteNameSuffix %>'

const routeLocale = getLocaleFromRoute(route, routesNameSeparator, defaultLocaleRouteNameSuffix, app.i18n.locales)
locale = routeLocale ? routeLocale : locale
} else if (useCookie) {
locale = getLocaleCookie() || locale
}

app.i18n.locale = locale

// Lazy-load translations
if (lazy) {
const { loadLanguageAsync } = require('./utils')

// Load fallback locale.
if (app.i18n.fallbackLocale && app.i18n.locale !== app.i18n.fallbackLocale) {
await loadLanguageAsync(context, app.i18n.fallbackLocale)
}

const messages = await loadLanguageAsync(context, app.i18n.locale)
await syncVuex(locale, messages)
return messages
} else {
// Sync Vuex
await syncVuex(locale, app.i18n.getLocaleMessage(locale))
}
await loadAndSetLocale(locale, { initialSetup: true })
}
18 changes: 14 additions & 4 deletions src/plugins/routing.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import './middleware'
import Vue from 'vue'

const STRATEGIES = <%= JSON.stringify(options.STRATEGIES) %>
const STRATEGY = '<%= options.strategy %>'
const vuex = <%= JSON.stringify(options.vuex) %>
const routesNameSeparator = '<%= options.routesNameSeparator %>'
const defaultLocale = '<%= options.defaultLocale %>'

function localePathFactory (i18nPath, routerPath) {
const STRATEGIES = <%= JSON.stringify(options.STRATEGIES) %>
const STRATEGY = '<%= options.strategy %>'
const defaultLocale = '<%= options.defaultLocale %>'
const defaultLocaleRouteNameSuffix = '<%= options.defaultLocaleRouteNameSuffix %>'

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

if (STRATEGY === STRATEGIES.NO_PREFIX && locale && locale !== this[i18nPath].locale) {
console.warn('[<%= options.MODULE_NAME %>] Passing non-current locale to localePath is unsupported when using no_prefix strategy')
}

locale = locale || this[i18nPath].locale

if (!locale) return

// If route parameters is a string, use it as the route's name
Expand All @@ -22,7 +28,7 @@ function localePathFactory (i18nPath, routerPath) {
}

// Build localized route options
let name = route.name + routesNameSeparator + locale
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) {
Expand All @@ -49,6 +55,10 @@ function switchLocalePathFactory (i18nPath) {
const LOCALE_CODE_KEY = '<%= options.LOCALE_CODE_KEY %>'

return function switchLocalePath (locale) {
if (STRATEGY === STRATEGIES.NO_PREFIX && locale && locale !== this[i18nPath].locale) {
console.warn('[<%= options.MODULE_NAME %>] Passing non-current locale to switchLocalePath is unsupported when using no_prefix strategy')
}

const name = this.getRouteBaseName()
if (!name) {
return ''
Expand Down
57 changes: 6 additions & 51 deletions src/templates/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ middleware['i18n'] = async (context) => {
}

// 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 %>
Expand Down Expand Up @@ -37,49 +39,7 @@ middleware['i18n'] = async (context) => {
const routeLocale = getLocaleFromRoute(route, routesNameSeparator, defaultLocaleRouteNameSuffix, locales)

const { useCookie, cookieKey, alwaysRedirect, fallbackLocale } = detectBrowserLanguage

const getLocaleCookie = () => {
if (useCookie) {
if (process.client) {
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]
}
}
return null
}

const switchLocale = async (newLocale) => {
// Abort if different domains option enabled
if (app.i18n.differentDomains) {
return
}

// Abort if newLocale did not change
if (newLocale === app.i18n.locale) {
return
}

const oldLocale = app.i18n.locale
app.i18n.beforeLanguageSwitch(oldLocale, newLocale)
if (useCookie) {
app.i18n.setLocaleCookie(newLocale)
}
// Lazy-loading enabled
if (lazy) {
const { loadLanguageAsync } = require('./utils')
const messages = await loadLanguageAsync(context, newLocale)
app.i18n.locale = newLocale
app.i18n.onLanguageSwitched(oldLocale, newLocale)
await syncVuex(newLocale, messages)
} else {
// Lazy-loading disabled
app.i18n.locale = newLocale
app.i18n.onLanguageSwitched(oldLocale, newLocale)
await syncVuex(newLocale, app.i18n.getLocaleMessage(newLocale))
}
}
const { getLocaleCookie } = app.i18n

if (detectBrowserLanguage) {
let browserLocale
Expand All @@ -97,7 +57,6 @@ middleware['i18n'] = async (context) => {
if (browserLocale) {
// Handle cookie option to prevent multiple redirections
if (!useCookie || alwaysRedirect || !getLocaleCookie()) {
const routeName = route && route.name ? app.getRouteBaseName(route) : 'index'
let redirectToLocale = fallbackLocale

// Use browserLocale if we support it, otherwise use fallbackLocale
Expand All @@ -108,11 +67,8 @@ middleware['i18n'] = async (context) => {
if (redirectToLocale && locales.includes(redirectToLocale)) {
if (redirectToLocale !== app.i18n.locale) {
// We switch the locale before redirect to prevent loops
await switchLocale(redirectToLocale)

redirect(app.localePath(Object.assign({}, route , {
name: routeName
}), redirectToLocale))
await app.i18n.setLocale(redirectToLocale)
}
} else if (useCookie && !getLocaleCookie()) {
app.i18n.setLocaleCookie(redirectToLocale)
}
Expand All @@ -121,7 +77,6 @@ middleware['i18n'] = async (context) => {
}
}
}
}

await switchLocale(routeLocale ? routeLocale : locale)
await app.i18n.setLocale(routeLocale ? routeLocale : locale)
}
Loading

0 comments on commit 998011e

Please sign in to comment.