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
2 changes: 1 addition & 1 deletion docs/content/en/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ All [Vue I18n properties and methods](http://kazupon.github.io/vue-i18n/api/#vue
- locale: (type: `string`)
- **Returns**: `string`

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

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

Expand Down
10 changes: 10 additions & 0 deletions docs/content/en/options-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,16 @@ 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.

## `prefixAndDefaultRule`

- type: `object`
- default: `'default'`

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

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

## `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 - [prefixAndDefaultRule](../options-reference#prefixanddefaultrule)

### Configuration

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"scripts": {
"dev:basic": "nuxt -c ./test/fixture/basic/nuxt.config.js",
"dev:redirect": "nuxt -c ./test/fixture/disable-redirect/nuxt.config.js",
"dev:basic:generate": "nuxt generate -c ./test/fixture/basic/nuxt.config.js",
"dev:basic:start": "nuxt start -c ./test/fixture/basic/nuxt.config.js",
"start:dist": "jiti ./test/utils/http-server-internal.js --port 8080 -v dist",
Expand Down
3 changes: 3 additions & 0 deletions src/helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export const DEFAULT_OPTIONS = {
defaultDirection: 'ltr',
routesNameSeparator: '___',
defaultLocaleRouteNameSuffix: 'default',
prefixAndDefaultRules: {
routing: 'default'
},
sortRoutes: true,
strategy: STRATEGY_PREFIX_EXCEPT_DEFAULT,
lazy: false,
Expand Down
10 changes: 7 additions & 3 deletions src/templates/plugin.main.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
loadLanguageAsync,
resolveBaseUrl,
registerStore,
mergeAdditionalMessages
mergeAdditionalMessages, getHelpers
} from './plugin.utils'
// @ts-ignore
import { joinURL } from '~i18n-ufo'
Expand Down Expand Up @@ -153,14 +153,18 @@ export default async (context) => {
let redirectPath = ''

const isStaticGenerate = process.static && process.server
const isDifferentLocale = getLocaleFromRoute(route) !== newLocale
const { isPrefixAndDefault, isDefaultLocale } = getHelpers(newLocale)

// 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 prefixAndDefaultRules.routing option to prefix.
(isDifferentLocale || (isPrefixAndDefault && isDefaultLocale && options.prefixAndDefaultRules.routing === 'default'))
) {
// 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.
Expand Down
52 changes: 43 additions & 9 deletions src/templates/plugin.routing.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import './middleware'
import Vue from 'vue'
import { Constants, nuxtOptions, options } from './options'
import { getDomainFromLocale } from './plugin.utils'
import { getDomainFromLocale, getHelpers } 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, isPrefixExceptDefault } = 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 && (isPrefixExceptDefault || (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 Down Expand Up @@ -123,6 +128,7 @@ function switchLocalePath (locale) {

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 } = options
const { isPrefixAndDefault, isDefaultLocale } = getHelpers(locale)
const shouldSwitchToPrefix = prefixAndDefaultRules.routing === 'prefix'

if (isPrefixAndDefault && isDefaultLocale && shouldSwitchToPrefix) {
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 @@ -167,17 +183,35 @@ function getRouteBaseName (givenRoute) {
/**
* @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 } = options
const isPrefixedPath = new RegExp(`^/${defaultLocale}(/|$)`).test(currentPath)

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

return !isPrefixedPath
}

/**
* @template {(...args: any[]) => any} T
* @param {T} targetFunction
Expand Down
17 changes: 16 additions & 1 deletion src/templates/plugin.utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import isHTTPS from 'is-https'
import { localeMessages, options } from './options'
import { Constants, localeMessages, options } from './options'
import { formatMessage } from './utils-common'
// @ts-ignore
import { hasProtocol } from '~i18n-ufo'
Expand Down Expand Up @@ -216,6 +216,21 @@ export function mergeAdditionalMessages (i18n, additionalMessages, localeCodes,
}
}

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

return {
isDefaultLocale,
isPrefixAndDefault,
isPrefixExceptDefault
}
}

/**
* @param {any} value
* @return {boolean}
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, '') || '/'
}
15 changes: 15 additions & 0 deletions test/fixture/disable-redirect/components/LangSwitcher.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<template>
<ul class="lang-switcher">
<li v-for="locale in $i18n.locales" :key="locale.code">
<nuxt-link :to="switchLocalePath(locale.code)">
{{ locale.code.toUpperCase() }}
</nuxt-link>
</li>
</ul>
</template>

<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>
21 changes: 21 additions & 0 deletions test/fixture/disable-redirect/layouts/default.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<template>
<div>
<p><strong>Default Layout</strong></p>

<NavBar />

<LangSwitcher />

<hr>

<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,
router: {
trailingSlash: true
},
i18n: {
prefixAndDefaultRule: 'default',
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