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
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: {
switchLocal: '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: { switchLocal } } = options
const { isPrefixAndDefault, isDefaultLocale } = getHelpers(locale)
const shouldSwitchToPrefix = switchLocal === '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: {
switchLocal: '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>
7 changes: 7 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { Locale }
export type Strategies = 'no_prefix' | 'prefix_except_default' | 'prefix' | 'prefix_and_default'
export type Directions = 'ltr' | 'rtl' | 'auto'
export type RedirectOnOptions = 'all' | 'root' | 'no prefix'
export type PrefixAndDefaultRule = 'default' | 'prefix'

export interface LocaleObject extends Record<string, any> {
code: Locale
Expand Down Expand Up @@ -52,6 +53,11 @@ export interface BaseOptions {
onLanguageSwitched?: (oldLocale: string, newLocale: string) => void
}

export interface PrefixAndDefaultRules {
switchLocal: PrefixAndDefaultRule
routing: PrefixAndDefaultRule
}

export interface Options extends BaseOptions {
baseUrl?: string | ((context: NuxtContext) => string)
detectBrowserLanguage?: DetectBrowserLanguageOptions | false
Expand All @@ -63,6 +69,7 @@ export interface Options extends BaseOptions {
}
}
parsePages?: boolean
prefixAndDefaultRules?: PrefixAndDefaultRules
rootRedirect?: string | null | RootRedirectOptions
routesNameSeparator?: string
skipSettingLocaleOnNavigate?: boolean,
Expand Down
2 changes: 1 addition & 1 deletion types/vue.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ interface NuxtI18nApi {
localePath(route: RawLocation, locale?: string): string
localeRoute(route: RawLocation, locale?: string): Route | undefined
localeLocation(route: RawLocation, locale?: string): Location | undefined
switchLocalePath(locale: string): string
switchLocalePath(locale: string, forcePrefix?: boolean): string
}

declare module 'vue-i18n' {
Expand Down