From 0a38b6dbfbdf549df4a7e0ff3684188134f8b8f0 Mon Sep 17 00:00:00 2001 From: Pavel Kuzmin Date: Fri, 19 Apr 2024 16:01:18 +0500 Subject: [PATCH 1/2] add new strategy prefix_regexp --- docs/content/docs/2.guide/1.index.md | 4 + docs/content/docs/3.options/2.routing.md | 1 + docs/content/docs/5.v7/3.options-reference.md | 1 + docs/content/docs/5.v7/6.strategies.md | 4 + playground/nuxt.config.ts | 1 + src/constants.ts | 2 + src/routing.ts | 97 ++++++++++++------- src/runtime/plugins/i18n.ts | 2 +- src/runtime/routing/compatibles/routing.ts | 17 +++- src/runtime/routing/extends/router.ts | 11 ++- src/runtime/routing/utils.ts | 7 ++ .../localize_routes.test.ts.snap | 21 ++++ test/pages/localize_routes.test.ts | 24 +++++ test/routing-utils.test.ts | 14 +++ 14 files changed, 164 insertions(+), 42 deletions(-) diff --git a/docs/content/docs/2.guide/1.index.md b/docs/content/docs/2.guide/1.index.md index fe7d95151..30dae7c00 100644 --- a/docs/content/docs/2.guide/1.index.md +++ b/docs/content/docs/2.guide/1.index.md @@ -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. diff --git a/docs/content/docs/3.options/2.routing.md b/docs/content/docs/3.options/2.routing.md index 7d5a89d17..754aeb56b 100644 --- a/docs/content/docs/3.options/2.routing.md +++ b/docs/content/docs/3.options/2.routing.md @@ -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` diff --git a/docs/content/docs/5.v7/3.options-reference.md b/docs/content/docs/5.v7/3.options-reference.md index 727649160..daa79ece6 100644 --- a/docs/content/docs/5.v7/3.options-reference.md +++ b/docs/content/docs/5.v7/3.options-reference.md @@ -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` diff --git a/docs/content/docs/5.v7/6.strategies.md b/docs/content/docs/5.v7/6.strategies.md index 0342a6a71..3ac578d6a 100644 --- a/docs/content/docs/5.v7/6.strategies.md +++ b/docs/content/docs/5.v7/6.strategies.md @@ -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. diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index a09f1a663..7e8882eb5 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -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, diff --git a/src/constants.ts b/src/constants.ts index 7434f02de..f57621a0a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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 diff --git a/src/routing.ts b/src/routing.ts index a67f0370b..6f06f79b8 100644 --- a/src/routing.ts +++ b/src/routing.ts @@ -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 diff --git a/src/runtime/plugins/i18n.ts b/src/runtime/plugins/i18n.ts index 7e28f921a..afcb9f7c1 100644 --- a/src/runtime/plugins/i18n.ts +++ b/src/runtime/plugins/i18n.ts @@ -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() diff --git a/src/runtime/routing/compatibles/routing.ts b/src/runtime/routing/compatibles/routing.ts index ff0dda506..f40b4736e 100644 --- a/src/runtime/routing/compatibles/routing.ts +++ b/src/runtime/routing/compatibles/routing.ts @@ -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) @@ -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 { @@ -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)) { @@ -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 { diff --git a/src/runtime/routing/extends/router.ts b/src/runtime/routing/extends/router.ts index 3f90a4b90..dfb13b6a5 100644 --- a/src/runtime/routing/extends/router.ts +++ b/src/runtime/routing/extends/router.ts @@ -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})?` @@ -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) { diff --git a/src/runtime/routing/utils.ts b/src/runtime/routing/utils.ts index 0eb6b1a12..cf997f28f 100644 --- a/src/runtime/routing/utils.ts +++ b/src/runtime/routing/utils.ts @@ -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 diff --git a/test/pages/__snapshots__/localize_routes.test.ts.snap b/test/pages/__snapshots__/localize_routes.test.ts.snap index 8f190912b..9d1a94547 100644 --- a/test/pages/__snapshots__/localize_routes.test.ts.snap +++ b/test/pages/__snapshots__/localize_routes.test.ts.snap @@ -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`] = ` [ { diff --git a/test/pages/localize_routes.test.ts b/test/pages/localize_routes.test.ts index f37c3d8d9..e56271b9d 100644 --- a/test/pages/localize_routes.test.ts +++ b/test/pages/localize_routes.test.ts @@ -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[] = [ diff --git a/test/routing-utils.test.ts b/test/routing-utils.test.ts index 0ee14620e..fc671e410 100644 --- a/test/routing-utils.test.ts +++ b/test/routing-utils.test.ts @@ -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`', () => { From 1960f79518b6108b651a506e619468a1a2bea657 Mon Sep 17 00:00:00 2001 From: Pavel Kuzmin Date: Fri, 19 Apr 2024 16:55:13 +0500 Subject: [PATCH 2/2] docs cleanup --- docs/content/docs/5.v7/3.options-reference.md | 1 - docs/content/docs/5.v7/6.strategies.md | 4 ---- 2 files changed, 5 deletions(-) diff --git a/docs/content/docs/5.v7/3.options-reference.md b/docs/content/docs/5.v7/3.options-reference.md index daa79ece6..727649160 100644 --- a/docs/content/docs/5.v7/3.options-reference.md +++ b/docs/content/docs/5.v7/3.options-reference.md @@ -96,7 +96,6 @@ 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` diff --git a/docs/content/docs/5.v7/6.strategies.md b/docs/content/docs/5.v7/6.strategies.md index 3ac578d6a..0342a6a71 100644 --- a/docs/content/docs/5.v7/6.strategies.md +++ b/docs/content/docs/5.v7/6.strategies.md @@ -25,10 +25,6 @@ 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.