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 new strategy prefix_regexp #2927

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/content/docs/2.guide/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ 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 (though the prefixed version will be preferred when `detectBrowserLanguage` is enabled).

### `prefix_regexp`

The prefix_regexp routing strategy generates two types of routes: one for the default language without a prefix, and a consolidated route for all other languages using a regular expression. This method reduces the number of routes by combining non-default locales into a single path, like /:locale(en-GB|ja|fr|nl|de)/about, simplifying management and enhancing scalability.

### Configuration

To configure the strategy, use the `strategy` option.
Expand Down
1 change: 1 addition & 0 deletions docs/content/docs/3.options/2.routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Routes generation strategy. Can be set to one of the following:
- `'prefix_except_default'`: locale prefix added for every locale except default
- `'prefix'`: locale prefix added for every locale
- `'prefix_and_default'`: locale prefix added for every locale and default
- `'prefix_regexp'`: generates two routes: a non-prefixed default language route, and a consolidated route for all other languages using a regex, like /:locale(en-GB|ja|fr|nl|de)/about.

## `customRoutes`

Expand Down
1 change: 1 addition & 0 deletions docs/content/docs/5.v7/3.options-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ Routes generation strategy. Can be set to one of the following:
- `'prefix_except_default'`: locale prefix added for every locale except default
- `'prefix'`: locale prefix added for every locale
- `'prefix_and_default'`: locale prefix added for every locale and default
- `'prefix_regexp'`: a single route for the default language without a prefix, and a combined route for all other languages using a regex prefix, such as /:locale(en-GB|ja|fr|nl|de)/about.
s00d marked this conversation as resolved.
Show resolved Hide resolved

## `lazy`

Expand Down
4 changes: 4 additions & 0 deletions docs/content/docs/5.v7/6.strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ 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 (though the prefixed version will be preferred when `detectBrowserLanguage` is enabled).

### prefix_regexp
s00d marked this conversation as resolved.
Show resolved Hide resolved

This strategy generates two types of routes: a non-prefixed route for the default language and a consolidated route for all other languages using a regex, such as /:locale(en-GB|ja|fr|nl|de)/about. It simplifies route management by minimizing the number of routes and maximizing scalability.

### Configuration

To configure the strategy, use the `strategy` option.
Expand Down
1 change: 1 addition & 0 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export default defineNuxtConfig({
// strategy: 'no_prefix',
// strategy: 'prefix',
// strategy: 'prefix_and_default',
// strategy: 'prefix_regexp',
strategy: 'prefix_except_default',
// rootRedirect: '/ja/about-ja',
dynamicRouteParams: true,
Expand Down
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ export const IS_HTTPS_PKG = 'is-https' as const
const STRATEGY_PREFIX = 'prefix'
const STRATEGY_PREFIX_EXCEPT_DEFAULT = 'prefix_except_default'
const STRATEGY_PREFIX_AND_DEFAULT = 'prefix_and_default'
const STRATEGY_REGEXP = 'prefix_regexp'
const STRATEGY_NO_PREFIX = 'no_prefix'
export const STRATEGIES = {
PREFIX: STRATEGY_PREFIX,
PREFIX_EXCEPT_DEFAULT: STRATEGY_PREFIX_EXCEPT_DEFAULT,
PREFIX_AND_DEFAULT: STRATEGY_PREFIX_AND_DEFAULT,
STRATEGY_REGEXP: STRATEGY_REGEXP,
NO_PREFIX: STRATEGY_NO_PREFIX
} as const

Expand Down
97 changes: 60 additions & 37 deletions src/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,53 +118,76 @@ export function localizeRoutes(routes: NuxtPage[], options: LocalizeRoutesParams
}

const localizedRoutes: (LocalizedRoute | NuxtPage)[] = []
for (const locale of componentOptions.locales) {
const localized: LocalizedRoute = { ...route, locale, parent }
const isDefaultLocale = defaultLocales.includes(locale)
const addDefaultTree = isDefaultLocale && options.strategy === 'prefix_and_default' && parent == null && !extra

// localize route again for strategy `prefix_and_default`
if (addDefaultTree && parent == null && !extra) {
localizedRoutes.push(...localizeRoute(route, { locales: [locale], extra: true }))
}

const nameSegments = [localized.name, options.routesNameSeparator, locale]
if (extra) {
nameSegments.push(options.routesNameSeparator, options.defaultLocaleRouteNameSuffix)
}
if (options.strategy === 'prefix_regexp') {
const defaultLocale = defaultLocales[0]
const nonDefaultLocales = componentOptions.locales.filter(l => l !== defaultLocale)
const localeRegex = nonDefaultLocales.join('|')
const defaultLocalized: LocalizedRoute = { ...route, locale: defaultLocale, parent }

localizedRoutes.push(defaultLocalized)

const combinedLocalized: LocalizedRoute = { ...route, locale: `/:locale(${localeRegex})`, parent }

// localize name if set
localized.name &&= join(...nameSegments)
combinedLocalized.path = `/:locale(${localeRegex})` + combinedLocalized.path
combinedLocalized.name &&= combinedLocalized.name + options.routesNameSeparator + 'locale'
combinedLocalized.path &&= adjustRoutePathForTrailingSlash(combinedLocalized, options.trailingSlash)
combinedLocalized.path = componentOptions.paths?.[`/:locale(${localeRegex})`] ?? combinedLocalized.path

// use custom path if found
localized.path = componentOptions.paths?.[locale] ?? localized.path
combinedLocalized.children &&= combinedLocalized.children.flatMap(child => {
return { ...child, ...{ name: child.name + options.routesNameSeparator + 'locale' } }
})

const localePrefixable = prefixLocalizedRoute(
{ defaultLocale: isDefaultLocale ? locale : options.defaultLocale, ...localized },
options,
extra
)
if (localePrefixable) {
localized.path = join('/', locale, localized.path)
localizedRoutes.push(combinedLocalized)
} else {
for (const locale of componentOptions.locales) {
const localized: LocalizedRoute = { ...route, locale, parent }
const isDefaultLocale = defaultLocales.includes(locale)
const addDefaultTree = isDefaultLocale && options.strategy === 'prefix_and_default' && parent == null && !extra

if (isDefaultLocale && options.strategy === 'prefix' && options.includeUnprefixedFallback) {
localizedRoutes.push({ ...route, locale, parent })
// localize route again for strategy `prefix_and_default`
if (addDefaultTree && parent == null && !extra) {
localizedRoutes.push(...localizeRoute(route, { locales: [locale], extra: true }))
}

const nameSegments = [localized.name, options.routesNameSeparator, locale]
if (extra) {
nameSegments.push(options.routesNameSeparator, options.defaultLocaleRouteNameSuffix)
}
}

localized.path &&= adjustRoutePathForTrailingSlash(localized, options.trailingSlash)
// localize name if set
localized.name &&= join(...nameSegments)

// remove parent path from child route
if (parentLocalized != null) {
localized.path = localized.path.replace(parentLocalized.path + '/', '')
}
// use custom path if found
localized.path = componentOptions.paths?.[locale] ?? localized.path

// localize child routes if set
localized.children &&= localized.children.flatMap(child =>
localizeRoute(child, { locales: [locale], parent: route, parentLocalized: localized, extra })
)
const localePrefixable = prefixLocalizedRoute(
{ defaultLocale: isDefaultLocale ? locale : options.defaultLocale, ...localized },
options,
extra
)
if (localePrefixable) {
localized.path = join('/', locale, localized.path)

localizedRoutes.push(localized)
if (isDefaultLocale && options.strategy === 'prefix' && options.includeUnprefixedFallback) {
localizedRoutes.push({ ...route, locale, parent })
}
}

localized.path &&= adjustRoutePathForTrailingSlash(localized, options.trailingSlash)

// remove parent path from child route
if (parentLocalized != null) {
localized.path = localized.path.replace(parentLocalized.path + '/', '')
}

// localize child routes if set
localized.children &&= localized.children.flatMap(child =>
localizeRoute(child, { locales: [locale], parent: route, parentLocalized: localized, extra })
)

localizedRoutes.push(localized)
}
}

// remove properties used for localization process
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/plugins/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export default defineNuxtPlugin({
vueI18nOptions.messages = vueI18nOptions.messages || {}
vueI18nOptions.fallbackLocale = vueI18nOptions.fallbackLocale ?? false

const getLocaleFromRoute = createLocaleFromRouteGetter()
const getLocaleFromRoute = createLocaleFromRouteGetter(runtimeI18n.strategy)
const getDefaultLocale = (defaultLocale: string) => defaultLocale || vueI18nOptions.locale || 'en-US'

const localeCookie = getI18nCookie()
Expand Down
17 changes: 16 additions & 1 deletion src/runtime/routing/compatibles/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export function resolveRoute(common: CommonComposableOptions, route: RouteLocati
_route = route
}

let localizedRoute = assign({} as RouteLocationPathRaw | RouteLocationNamedRaw, _route)
let localizedRoute = assign({} as (RouteLocationPathRaw & { params: any }) | RouteLocationNamedRaw, _route)

const isRouteLocationPathRaw = (val: RouteLocationPathRaw | RouteLocationNamedRaw): val is RouteLocationPathRaw =>
'path' in val && !!val.path && !('name' in val)
Expand All @@ -174,6 +174,11 @@ export function resolveRoute(common: CommonComposableOptions, route: RouteLocati
hash: resolvedRoute.hash
} as RouteLocationNamedRaw

if (defaultLocale !== _locale && strategy === 'prefix_regexp') {
// @ts-ignore
localizedRoute.params = { ...localizedRoute.params, ...{ locale: _locale } }
}

// @ts-expect-error
localizedRoute.state = (resolvedRoute as ResolveV4).state
} else {
Expand All @@ -185,6 +190,11 @@ export function resolveRoute(common: CommonComposableOptions, route: RouteLocati
localizedRoute.path = trailingSlash
? withTrailingSlash(localizedRoute.path, true)
: withoutTrailingSlash(localizedRoute.path, true)

if (defaultLocale !== _locale && strategy === 'prefix_regexp') {
// @ts-ignore
localizedRoute.params = { ...resolvedRoute.params, ...{ locale: _locale } }
}
}
} else {
if (!localizedRoute.name && !('path' in localizedRoute)) {
Expand All @@ -197,6 +207,11 @@ export function resolveRoute(common: CommonComposableOptions, route: RouteLocati
routesNameSeparator,
defaultLocaleRouteNameSuffix
})

if (defaultLocale !== _locale && strategy === 'prefix_regexp') {
// @ts-ignore
localizedRoute.params = { ...localizedRoute.params, ...{ locale: _locale } }
}
}

try {
Expand Down
11 changes: 8 additions & 3 deletions src/runtime/routing/extends/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { localeCodes } from '#build/i18n.options.mjs'
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded } from 'vue-router'
import { useRuntimeConfig } from 'nuxt/app'

export function createLocaleFromRouteGetter() {
export function createLocaleFromRouteGetter(strategy: string) {
const { routesNameSeparator, defaultLocaleRouteNameSuffix } = useRuntimeConfig().public.i18n
const localesPattern = `(${localeCodes.join('|')})`
const defaultSuffixPattern = `(?:${routesNameSeparator}${defaultLocaleRouteNameSuffix})?`
Expand All @@ -20,13 +20,18 @@ export function createLocaleFromRouteGetter() {
const getLocaleFromRoute = (route: RouteLocationNormalizedLoaded | RouteLocationNormalized | string): string => {
// extract from route name
if (isObject(route)) {
if (route.name) {
if (strategy === 'prefix_regexp') {
if (route.params.locale) {
return route.params.locale.toString()
}
} else if (route.name) {
const name = isString(route.name) ? route.name : route.name.toString()
const matches = name.match(regexpName)
if (matches && matches.length > 1) {
return matches[1]
}
} else if (route.path) {
}
if (route.path) {
// Extract from path
const matches = route.path.match(regexpPath)
if (matches && matches.length > 1) {
Expand Down
7 changes: 7 additions & 0 deletions src/runtime/routing/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ export function getLocaleRouteName(
defaultLocaleRouteNameSuffix
}: { defaultLocale: string; strategy: Strategies; routesNameSeparator: string; defaultLocaleRouteNameSuffix: string }
) {
if (strategy === 'prefix_regexp') {
let name = getRouteName(routeName).replace(`${routesNameSeparator}locale`, '')
if (locale !== defaultLocale) {
name += `${routesNameSeparator}locale`
}
return name
}
let name = getRouteName(routeName) + (strategy === 'no_prefix' ? '' : routesNameSeparator + locale)
if (locale === defaultLocale && strategy === 'prefix_and_default') {
name += routesNameSeparator + defaultLocaleRouteNameSuffix
Expand Down
21 changes: 21 additions & 0 deletions test/pages/__snapshots__/localize_routes.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,27 @@ exports[`localizeRoutes > strategy: "prefix_except_default" > should be localize
]
`;

exports[`localizeRoutes > strategy: "prefix_regexp" > should be localized routing 1`] = `
[
{
"name": "home",
"path": "/",
},
{
"name": "home___locale",
"path": "/:locale(ja)",
},
{
"name": "about",
"path": "/about",
},
{
"name": "about___locale",
"path": "/:locale(ja)/about",
},
]
`;

exports[`localizeRoutes > trailing slash > should be localized routing 1`] = `
[
{
Expand Down
24 changes: 24 additions & 0 deletions test/pages/localize_routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,30 @@ describe('localizeRoutes', function () {
})
})

describe('strategy: "prefix_regexp"', function () {
it('should be localized routing', function () {
const routes: NuxtPage[] = [
{
path: '/',
name: 'home'
},
{
path: '/about',
name: 'about'
}
]
const localeCodes = ['en', 'ja']
const localizedRoutes = localizeRoutes(routes, {
...nuxtOptions,
defaultLocale: 'en',
strategy: 'prefix_regexp',
locales: localeCodes
})

expect(localizedRoutes).toMatchSnapshot()
})
})

describe('Route options resolver: routing disable', () => {
it('should be disabled routing', () => {
const routes: NuxtPage[] = [
Expand Down
14 changes: 14 additions & 0 deletions test/routing-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,20 @@ describe('getLocaleRouteName', () => {
})
})

describe('strategy: prefix_regexp', () => {
it('should be `route1`', () => {
assert.equal(
utils.getLocaleRouteName('route1', 'en', {
defaultLocale: 'en',
strategy: 'prefix_regexp',
routesNameSeparator: '___',
defaultLocaleRouteNameSuffix: 'default'
}),
'route1'
)
})
})

describe('irregular', () => {
describe('route name is null', () => {
it('should be ` (null)___en___default`', () => {
Expand Down