Skip to content
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.

## `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

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