Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add possibility to disable default redirect for prefix_and_default strategy #1437

Open
wants to merge 9 commits into
base: v7
Choose a base branch
from
8 changes: 7 additions & 1 deletion docs/content/en/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,18 @@ All [Vue I18n properties and methods](http://kazupon.github.io/vue-i18n/api/#vue

- **Arguments**:
- locale: (type: `string`)
- forcePrefix: (type: `boolean`)
rchl marked this conversation as resolved.
Show resolved Hide resolved
- **Returns**: `string`

Returns path of the current route for specified `locale`.
Returns path of the current route for specified `locale`.

If `forcePrefix` is set to `true`, it returns the path with the prefix. Useful for `prefix_and_default` strategy.
You can also set global behavior through option prefixAndDefaultRules.

See also [Basic usage - nuxt-link](../basic-usage#nuxt-link).

See more details about [prefixAndDefaultRules](../options-reference#prefixanddefaultrules).

See type definition for [Location](https://github.com/vuejs/vue-router/blob/f40139c27a9736efcbda69ec136cb00d8e00fa97/types/router.d.ts#L125).

### getRouteBaseName()
Expand Down
12 changes: 12 additions & 0 deletions docs/content/en/options-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,18 @@ Set this to `true` when using different domains for each locale. If enabled, no

Whether [custom paths](/routing#custom-paths) are extracted from page files using babel parser.

## `prefixAndDefaultRules`

- type: `object`
- default: `{ routing: 'default', switchLocale: 'default' }`
rchl marked this conversation as resolved.
Show resolved Hide resolved

Modification of the standard behavior of the `prefix_and_default` strategy.

By setting the value `routing = 'prefix'` we are no longer being redirected from prefixed path.
If there is no prefix then the other routers will also have no prefix.

By setting the value `switchLocale = 'prefix'` the `switchLocalePath()` method adds a prefix to the current route.

## `pages`

- type: `object`
Expand Down
4 changes: 3 additions & 1 deletion docs/content/en/strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ With this strategy, all routes will have a locale prefix.

### prefix_and_default

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 (though the prefixed version will be preferred when `detectBrowserLanguage` is enabled.
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 (though the prefixed version will be preferred when `detectBrowserLanguage` is enabled.)

The behavior of the strategy can be modified, more - [prefixAndDefaultRules](../options-reference#prefixanddefaultrules)

### Configuration

Expand Down
4 changes: 4 additions & 0 deletions src/helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export const DEFAULT_OPTIONS = {
defaultDirection: 'ltr',
routesNameSeparator: '___',
defaultLocaleRouteNameSuffix: 'default',
prefixAndDefaultRules: {
switchLocale: 'default',
routing: 'default'
},
sortRoutes: true,
strategy: STRATEGY_PREFIX_EXCEPT_DEFAULT,
lazy: false,
Expand Down
14 changes: 9 additions & 5 deletions src/templates/plugin.main.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,18 +153,22 @@ export default async (context) => {
let redirectPath = ''

const isStaticGenerate = process.static && process.server
const isDifferentLocale = getLocaleFromRoute(route) !== newLocale
const isPrefixAndDefaultStrategy = options.strategy === Constants.STRATEGIES.PREFIX_AND_DEFAULT
const isDefaultLocale = newLocale === options.defaultLocale

// Decide whether we should redirect to a different route.
if (
!isStaticGenerate &&
!app.i18n.differentDomains &&
options.strategy !== Constants.STRATEGIES.NO_PREFIX &&
// Skip if already on the new locale unless the strategy is "prefix_and_default" and this is the default
// locale, in which case we might still redirect as we prefer unprefixed route in this case.
(getLocaleFromRoute(route) !== newLocale || (options.strategy === Constants.STRATEGIES.PREFIX_AND_DEFAULT && newLocale === options.defaultLocale))
// locale, in which case we might still redirect as we prefer unprefixed route in this case, but let user a
// possibility to disable this behavior by switching disableDefaultRedirect option to true.
(isDifferentLocale || (isPrefixAndDefaultStrategy && isDefaultLocale))
) {
// The current route could be 404 in which case attempt to find matching route using the full path since
// "switchLocalePath" can only find routes if the current route exists.
const routePath = app.switchLocalePath(newLocale) || app.localePath(route.fullPath, newLocale)
rchl marked this conversation as resolved.
Show resolved Hide resolved
const routePath = app.localePath(route.fullPath, newLocale)

if (routePath && routePath !== route.fullPath && !routePath.startsWith('//')) {
redirectPath = routePath
}
Expand Down
65 changes: 56 additions & 9 deletions src/templates/plugin.routing.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import './middleware'
import Vue from 'vue'
import { Constants, nuxtOptions, options } from './options'
import { getDomainFromLocale } from './plugin.utils'
import { removeLocaleFromPath } from './utils-common'
// @ts-ignore
import { withoutTrailingSlash, withTrailingSlash } from '~i18n-ufo'

Expand Down Expand Up @@ -44,7 +45,7 @@ function resolveRoute (route, locale) {
return
}

const { i18n } = this
const { i18n, route: currentRoute } = this

locale = locale || i18n.locale

Expand All @@ -68,19 +69,21 @@ function resolveRoute (route, locale) {
if (localizedRoute.path && !localizedRoute.name) {
const resolvedRoute = this.router.resolve(localizedRoute).route
const resolvedRouteName = this.getRouteBaseName(resolvedRoute)
const forceDefaultRoute = shouldForceDefaultRoute(currentRoute.fullPath)

if (resolvedRouteName) {
localizedRoute = {
name: getLocaleRouteName(resolvedRouteName, locale),
name: getLocaleRouteName(resolvedRouteName, locale, forceDefaultRoute),
params: resolvedRoute.params,
query: resolvedRoute.query,
hash: resolvedRoute.hash
}
} else {
const isDefaultLocale = locale === options.defaultLocale
const { isDefaultLocale, isPrefixAndDefault } = getHelpers(locale)
// if route has a path defined but no name, resolve full route using the path
const isPrefixed =
// don't prefix default locale
!(isDefaultLocale && [Constants.STRATEGIES.PREFIX_EXCEPT_DEFAULT, Constants.STRATEGIES.PREFIX_AND_DEFAULT].includes(options.strategy)) &&
// don't prefix default locale, if not forced
!(isDefaultLocale && isPrefixAndDefault && forceDefaultRoute) &&
// no prefix for any language
!(options.strategy === Constants.STRATEGIES.NO_PREFIX) &&
// no prefix for different domains
Expand All @@ -95,7 +98,9 @@ function resolveRoute (route, locale) {
localizedRoute.name = this.getRouteBaseName()
}

localizedRoute.name = getLocaleRouteName(localizedRoute.name, locale)
const forceDefaultRoute = shouldForceDefaultRoute(currentRoute.fullPath)

localizedRoute.name = getLocaleRouteName(localizedRoute.name, locale, forceDefaultRoute)

const { params } = localizedRoute
if (params && params['0'] === undefined && params.pathMatch) {
Expand All @@ -115,14 +120,15 @@ function resolveRoute (route, locale) {
* @this {import('../../types/internal').PluginProxy}
* @type {Vue['switchLocalePath']}
*/
function switchLocalePath (locale) {
function switchLocalePath (locale, forcePrefix = false) {
const name = this.getRouteBaseName()
if (!name) {
return ''
}

const { i18n, route, store } = this
const { params, ...routeCopy } = route

let langSwitchParams = {}
if (options.vuex && options.vuex.syncRouteParams && store) {
langSwitchParams = store.getters[`${options.vuex.moduleName}/localeRouteParams`](locale)
Expand All @@ -137,6 +143,16 @@ function switchLocalePath (locale) {
})
let path = this.localePath(baseRoute, locale)

const { prefixAndDefaultRules: { switchLocale } } = options
const { isPrefixAndDefault, isDefaultLocale } = getHelpers(locale)
const shouldSwitchToPrefix = switchLocale === 'prefix'

if (isPrefixAndDefault && isDefaultLocale && (shouldSwitchToPrefix || forcePrefix)) {
const cleanPath = removeLocaleFromPath(path, [locale])
const localizedPath = `/${locale}${cleanPath}`
path = nuxtOptions.trailingSlash ? withTrailingSlash(localizedPath) : withoutTrailingSlash(localizedPath)
}

// Handle different domains
if (i18n.differentDomains) {
const getDomainOptions = {
Expand Down Expand Up @@ -164,20 +180,51 @@ function getRouteBaseName (givenRoute) {
return route.name.split(options.routesNameSeparator)[0]
}

/**
* @param {string} locale
*/
function getHelpers (locale = '') {
const isDefaultLocale = locale === options.defaultLocale
const isPrefixAndDefault = options.strategy === Constants.STRATEGIES.PREFIX_AND_DEFAULT

return {
isDefaultLocale,
isPrefixAndDefault
}
}

/**
* @param {string | undefined} routeName
* @param {string} locale
* @param {boolean} forceDefaultName
*/
function getLocaleRouteName (routeName, locale) {
function getLocaleRouteName (routeName, locale, forceDefaultName = true) {
const { isDefaultLocale, isPrefixAndDefault } = getHelpers(locale)
let name = routeName + (options.strategy === Constants.STRATEGIES.NO_PREFIX ? '' : options.routesNameSeparator + locale)

if (locale === options.defaultLocale && options.strategy === Constants.STRATEGIES.PREFIX_AND_DEFAULT) {
if (isDefaultLocale && isPrefixAndDefault && forceDefaultName) {
name += options.routesNameSeparator + options.defaultLocaleRouteNameSuffix
}

return name
}

/**
* @param {string} currentPath
* @return {boolean}
*/
function shouldForceDefaultRoute (currentPath) {
const { isPrefixAndDefault } = getHelpers()
const { defaultLocale, prefixAndDefaultRules: { routing } } = options
const isPrefixedPath = new RegExp(`^/${defaultLocale}(/|$)`).test(currentPath)

if (!isPrefixAndDefault || routing === 'default') {
return true
}

return !isPrefixedPath
}

/**
* @template {(...args: any[]) => any} T
* @param {T} targetFunction
Expand Down
11 changes: 11 additions & 0 deletions src/templates/utils-common.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,14 @@ export function setLocaleCookie (locale, res, { useCookie, cookieDomain, cookieK
res.setHeader('Set-Cookie', headers)
}
}

/**
* @param {string} pathString
* @param {readonly string[]} localeCodes
* @return {string}
*/
export function removeLocaleFromPath (pathString, localeCodes) {
const regexp = new RegExp(`^(\\/${localeCodes.join('|\\/')})(?=\\/|$)`)

return pathString.replace(regexp, '') || '/'
}
26 changes: 26 additions & 0 deletions test/fixture/disable-redirect/components/LangSwitcher.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<template>
<ul class="lang-switcher">
<li v-for="locale in $i18n.locales" :key="locale.code">
<nuxt-link :to="switchLocalePath(locale.code, forcePrefix)">
{{ locale.code.toUpperCase() }}
</nuxt-link>
</li>
</ul>
</template>

<script>
export default {
props: {
forcePrefix: {
type: Boolean,
default: false
}
}
}
</script>

<style scoped>
.lang-switcher {
margin: 20px;
}
</style>
37 changes: 37 additions & 0 deletions test/fixture/disable-redirect/components/NavBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<template>
<div class="navbar">
<nuxt-link to="/">
Home Default
</nuxt-link>
<nuxt-link v-for="link in links" :key="link.label" tag="a" :to="localePath(link.path)">
{{ link.label }}
</nuxt-link>
</div>
</template>

<script>
export default {
computed: {
links () {
return [
{ label: 'Home', path: '/' },
{ label: 'About', path: '/about' },
{ label: 'Foo', path: '/foo' },
{ label: 'FooBar', path: '/foo/bar' }
]
}
}
}
</script>

<style>
.navbar {
display: flex;
align-items: center;
justify-content: flex-start;
}

.navbar a {
padding: 0 10px;
}
</style>
23 changes: 23 additions & 0 deletions test/fixture/disable-redirect/layouts/default.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<template>
<div>
<p><strong>Default Layout</strong></p>

<NavBar />

<LangSwitcher />

<hr>

<LangSwitcher force-prefix />

<Nuxt />
</div>
</template>

<script>
import LangSwitcher from '../components/LangSwitcher'
import NavBar from '../components/NavBar'
export default {
components: { NavBar, LangSwitcher }
}
</script>
31 changes: 31 additions & 0 deletions test/fixture/disable-redirect/nuxt.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { resolve } from 'path'
import BaseConfig from '../base.config'

/** @type {import('@nuxt/types').NuxtConfig} */
const config = {
...BaseConfig,
i18n: {
prefixAndDefaultRules: {
switchLocale: 'default',
routing: 'prefix'
},
strategy: 'prefix_and_default',
locales: [
{
code: 'en',
iso: 'en',
name: 'English'
},
{
code: 'fr',
iso: 'fr-FR',
name: 'Français'
}
],
defaultLocale: 'en'
},
buildDir: resolve(__dirname, '.nuxt'),
srcDir: __dirname
}

module.exports = config
5 changes: 5 additions & 0 deletions test/fixture/disable-redirect/pages/about.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<div>
<h1>About</h1>
</div>
</template>
5 changes: 5 additions & 0 deletions test/fixture/disable-redirect/pages/foo/bar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<div>
<h1>Bar</h1>
</div>
</template>
5 changes: 5 additions & 0 deletions test/fixture/disable-redirect/pages/foo/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<div>
<h1>Foo</h1>
</div>
</template>
5 changes: 5 additions & 0 deletions test/fixture/disable-redirect/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<div>
<h1>Home</h1>
</div>
</template>
Loading