From 95b915fc03254d22d562bd5e4a2b387e212f470f Mon Sep 17 00:00:00 2001 From: Dmitry Istomin Date: Wed, 13 Sep 2023 17:32:39 +0300 Subject: [PATCH] feat: add customRoutes named-config (#2407) --- docs/content/2.guide/4.custom-paths.md | 153 ++++++++++++++++- .../2.guide/5.ignoring-localized-routes.md | 24 ++- docs/content/3.options/2.routing.md | 4 +- playground/nuxt.config.ts | 36 ++++ src/pages.ts | 69 +++++++- src/types.ts | 2 +- .../__snapshots__/custom_route.test.ts.snap | 159 ++++++++++++++++++ test/pages/custom_route.test.ts | 132 +++++++++++++++ 8 files changed, 569 insertions(+), 10 deletions(-) diff --git a/docs/content/2.guide/4.custom-paths.md b/docs/content/2.guide/4.custom-paths.md index 277a9a126..a7cb9b895 100644 --- a/docs/content/2.guide/4.custom-paths.md +++ b/docs/content/2.guide/4.custom-paths.md @@ -4,7 +4,13 @@ Customize the names of the paths for specific locale. --- -In some cases, you might want to translate URLs in addition to having them prefixed with the locale code. There are 2 ways of configuring custom paths for your [Module configuration](#nodule-configuration) or your pages [Page component](#page-component). +In some cases, you might want to translate URLs in addition to having them prefixed with the locale code. There are 3 ways of configuring custom paths: + +[Module configuration](#module-configuration) (`customRoutes: 'config'`) + +[Module named configuration](#module-named-configuration) (`customRoutes: 'named-config'`) + +[Page component](#page-component) (`customRoutes: 'page'`) ::alert{type="warning"} Custom paths are not supported when using the `no-prefix` [strategy](/guide/routing-strategies). @@ -187,6 +193,151 @@ export default defineNuxtConfig({ }) ``` +### Module named configuration + +Make sure you set the `customRoutes` option to `named-config` and add your custom paths in the `pages` option: + +```ts {}[nuxt.config.ts] +export default defineNuxtConfig({ + // ... + + i18n: { + customRoutes: 'config', // disable custom route with page components + pages: { + about: { + en: '/about-us', // -> accessible at /about-us (no prefix since it's the default locale) + fr: '/a-propos', // -> accessible at /fr/a-propos + es: '/sobre' // -> accessible at /es/sobre + } + } + }, + + // ... +}) +``` + +Note that each key within the `pages` object should **correspond to the route name**. + +Customized route paths **must start with a `/`** and **not include the locale prefix**. + +#### Example 1: Basic URL localization + +You have some routes with the following `pages` directory: + +```asciidoc +pages/ +├── about.vue +├── me.vue +├── services/ +├──── index.vue +├──── advanced.vue +|---- [param].vue + +``` +You would need to set up your `pages` property as follows: + +```ts {}[nuxt.config.ts] +export default defineNuxtConfig({ + // ... + + i18n: { + customRoutes: 'named-config', + pages: { + about: { + fr: '/a-propos', + }, + me: { + fr: '/je', + }, + 'services': { + fr: '/offres', + } + 'services-advanced': { + fr: '/offres/avancee', + } + 'services-param': { + fr: '/offres/:param()', + } + } + }, + + // ... +}) +``` + +Nuxt generates routes names based on you files structure +Basically it just removes special characters and join all segments with '-' + +if you not sure about routes names just add that simple module before `'@nuxtjs/i18n'`: + +```ts{}[nuxt.config.ts] +export default defineNuxtConfig({ + modules: [ + (_, nuxt) => { + nuxt.hook('pages:extend', pages => { + let pagesCopy = JSON.parse(JSON.stringify(pages)) + function processRoutes(routes) { + return routes.map(r => { + if (r.children) { + r.children = processRoutes(r.children) + } + return { + name: r.name, + path: r.path, + children: r.children + } + }) + } + console.log('PAGES:') + console.log(JSON.stringify(processRoutes(pagesCopy), null, 4)) + }) + }, + '@nuxtjs/i18n', + ], +}) +``` + +::alert{type="warning"} +All the URL should start with `/` +:: + +#### Example 3: Dynamic Routes + +Say you have some dynamic routes like: + +```asciidoc +pages/ +├── blog/ +├──── [date]/ +├────── [slug].vue +├── news/ +├──── [[slug]].vue +``` + +Here's how you would configure these particular pages in the configuration: + +```ts {}[nuxt.config.ts] +export default defineNuxtConfig({ + // ... + + i18n: { + customRoutes: 'named-config', + pages: { + 'blog-date-slug': { + // params need to be put back here as you would declare with Vue Router + // https://router.vuejs.org/guide/essentials/dynamic-matching.html + ja: '/blog/tech/:date()/:slug()' + }, + 'news-slug':{ + ja: '/news-ja/:slug?' + } + } + }, + + // ... +}) +``` + ### Page component You can use the `defineI18nRoute` compiler macro to set some custom paths for each page component. diff --git a/docs/content/2.guide/5.ignoring-localized-routes.md b/docs/content/2.guide/5.ignoring-localized-routes.md index 89428e448..b1bfcf944 100644 --- a/docs/content/2.guide/5.ignoring-localized-routes.md +++ b/docs/content/2.guide/5.ignoring-localized-routes.md @@ -25,7 +25,19 @@ If you'd like some pages to be available in some languages only, you can configu ::code-block{label="Module configuration"} ```js {}[nuxt.config.js] i18n: { - customRoutes: false, + customRoutes: 'config', + pages: { + about: { + en: false, + } + } + } + ``` + :: + ::code-block{label="Module named configuration"} + ```js {}[nuxt.config.js] + i18n: { + customRoutes: 'named-config', pages: { about: { en: false, @@ -56,4 +68,14 @@ If you'd like some pages to be available in some languages only, you can configu } ``` :: + ::code-block{label="Module named configuration"} + ```js {}[nuxt.config.js] + i18n: { + customRoutes: 'named-config', + pages: { + about: false + } + } + ``` + :: :: diff --git a/docs/content/3.options/2.routing.md b/docs/content/3.options/2.routing.md index ec9311cad..84ae49bb6 100644 --- a/docs/content/3.options/2.routing.md +++ b/docs/content/3.options/2.routing.md @@ -79,7 +79,7 @@ Routes generation strategy. Can be set to one of the following: ## `customRoutes` -- type: `string` (`page` or `config`) | `undefined` +- type: `string` (`page`, `config` or `named-config`) | `undefined` - default: `page` Whether [custom paths](/guide/custom-paths) are extracted from page files @@ -89,7 +89,7 @@ Whether [custom paths](/guide/custom-paths) are extracted from page files - type: `object` - default: `{}` -If `customRoutes` option is disabled with `config`, the module will look for custom routes in the `pages` option. Refer to the [Routing](/guide/routing-strategies) for usage. +If `customRoutes` option is disabled with `config` or `named-config`, the module will look for custom routes in the `pages` option. Refer to the [Routing](/guide/routing-strategies) for usage. ## `skipSettingLocaleOnNavigate` diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 5b38d6f44..4908223bb 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -10,6 +10,7 @@ export default defineNuxtConfig({ (_, nuxt) => { console.log(nuxt.options._installedModules) }, + Module1, ModuleExperimental, LayerModule, @@ -88,6 +89,25 @@ export default defineNuxtConfig({ } ], */ + (_, nuxt) => { + nuxt.hook('pages:extend', pages => { + let pagesCopy = JSON.parse(JSON.stringify(pages)) + function processRoutes(routes) { + return routes.map(r => { + if (r.children) { + r.children = processRoutes(r.children) + } + return { + name: r.name, + path: r.path, + children: r.children + } + }) + } + console.log('PAGES:') + console.log(JSON.stringify(processRoutes(pagesCopy), null, 4)) + }) + }, '@nuxtjs/i18n', '@nuxt/devtools' ], @@ -171,6 +191,22 @@ export default defineNuxtConfig({ ja: '/about-ja' } }, + + // // NAMED-CONFIG + // customRoutes: 'named-config', + // pages: { + // history: { + // ja: '/history-ja' + // }, + // about: { + // ja: '/about-ja' + // }, + // 'category-id': { + // ja: '/category-ja/:id()' + // } + // }, + // // END OF NAMED-CONFIG + // differentDomains: true, skipSettingLocaleOnNavigate: true, detectBrowserLanguage: false, diff --git a/src/pages.ts b/src/pages.ts index c69fa7e72..7bf98f77a 100644 --- a/src/pages.ts +++ b/src/pages.ts @@ -122,13 +122,27 @@ export function getRouteOptionsResolver( ): RouteOptionsResolver { const { pages, defaultLocale, customRoutes } = options - const useConfig = customRoutes === 'config' - debug('getRouteOptionsResolver useConfig', useConfig) + let customRoutesProvider = 'page' + if (customRoutes && ['config', 'named-config', 'page'].includes(customRoutes)) { + customRoutesProvider = customRoutes + } else if (customRoutes) { + console.warn( + formatMessage( + `Unknown customRoutes property (${JSON.stringify(customRoutes)}), will be reset to default ("page")` + ) + ) + } + + debug('getRouteOptionsResolver customRoutes:', customRoutes) return (route, localeCodes): ComputedRouteOptions | null => { - const ret = useConfig - ? getRouteOptionsFromPages(ctx, route, localeCodes, pages, defaultLocale) - : getRouteOptionsFromComponent(route, localeCodes) + const routesGetters = { + config: () => getRouteOptionsFromPages(ctx, route, localeCodes, pages, defaultLocale), + 'named-config': () => getRouteOptionsFromRoutesNames(ctx, route, localeCodes, pages, defaultLocale), + page: () => getRouteOptionsFromComponent(route, localeCodes) + } + + const ret = routesGetters[customRoutesProvider]() debug('getRouteOptionsResolver resolved', route.path, route.name, ret) return ret } @@ -198,6 +212,51 @@ function getRouteOptionsFromPages( return options } +function getRouteOptionsFromRoutesNames( + ctx: NuxtPageAnalyzeContext, + route: I18nRoute, + localeCodes: string[], + pages: CustomRoutePages, + defaultLocale: string +) { + const options: ComputedRouteOptions = { + locales: localeCodes, + paths: {} + } + const pageOptions = pages[route.name] + + // routing disabled + if (pageOptions === false) { + return null + } + + // skip if no page options defined + if (!pageOptions) { + return options + } + + // remove disabled locales from page options + options.locales = options.locales.filter(locale => pageOptions[locale] !== false) + + // construct paths object + for (const locale of options.locales) { + const customLocalePath = pageOptions[locale] + if (isString(customLocalePath)) { + // set custom path if any + options.paths[locale] = customLocalePath + continue + } + + const customDefaultLocalePath = pageOptions[defaultLocale] + if (isString(customDefaultLocalePath)) { + // set default locale's custom path if any + options.paths[locale] = customDefaultLocalePath + } + } + + return options +} + function getRouteOptionsFromComponent(route: I18nRoute, localeCodes: string[]) { debug('getRouteOptionsFromComponent', route) const file = route.component || route.file diff --git a/src/types.ts b/src/types.ts index 0bd475975..226d1e02a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -86,7 +86,7 @@ export type NuxtI18nOptions = { langDir?: string | null lazy?: boolean pages?: CustomRoutePages - customRoutes?: 'page' | 'config' + customRoutes?: 'page' | 'config' | 'named-config' /** * @internal */ diff --git a/test/pages/__snapshots__/custom_route.test.ts.snap b/test/pages/__snapshots__/custom_route.test.ts.snap index fa66d4b8b..9860ac8fb 100644 --- a/test/pages/__snapshots__/custom_route.test.ts.snap +++ b/test/pages/__snapshots__/custom_route.test.ts.snap @@ -248,6 +248,165 @@ exports[`Module configuration > the part of URL 1`] = ` ] `; +exports[`Module named configuration > dynamic parameters 1`] = ` +[ + { + "children": [], + "file": "/path/to/nuxt-app/pages/blog/[date]/[slug].vue", + "name": "blog-date-slug___en", + "path": "/blog/:date()/:slug()", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/blog/[date]/[slug].vue", + "name": "blog-date-slug___ja", + "path": "/ja/blog/tech/:date()/:slug()", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/blog/[date]/[slug].vue", + "name": "blog-date-slug___fr", + "path": "/fr/blog/:date()/:slug()", + }, +] +`; + +exports[`Module named configuration > simple 1`] = ` +[ + { + "children": [], + "file": "/path/to/nuxt-app/pages/about.vue", + "name": "about___en", + "path": "/about-us", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/about.vue", + "name": "about___ja", + "path": "/ja/about-us", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/about.vue", + "name": "about___fr", + "path": "/fr/a-propos", + }, +] +`; + +exports[`Module named configuration > the part of URL 1`] = ` +[ + { + "children": [], + "file": "/path/to/nuxt-app/pages/about.vue", + "name": "about___en", + "path": "/about", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/about.vue", + "name": "about___ja", + "path": "/ja/about", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/about.vue", + "name": "about___fr", + "path": "/fr/a-propos", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/services/coaching.vue", + "name": "services-coaching___en", + "path": "/services/coaching", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/services/coaching.vue", + "name": "services-coaching___ja", + "path": "/ja/services/coaching", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/services/coaching.vue", + "name": "services-coaching___fr", + "path": "/fr/offres/formation", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/services/development/app.vue", + "name": "services-development-app___en", + "path": "/services/development/app", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/services/development/app.vue", + "name": "services-development-app___ja", + "path": "/ja/services/development/app", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/services/development/app.vue", + "name": "services-development-app___fr", + "path": "/fr/offres/developement/app", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/services/development/index.vue", + "name": "services-development___en", + "path": "/services/development", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/services/development/index.vue", + "name": "services-development___ja", + "path": "/ja/services/development", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/services/development/index.vue", + "name": "services-development___fr", + "path": "/fr/offres/developement", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/services/development/website.vue", + "name": "services-development-website___en", + "path": "/services/development/website", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/services/development/website.vue", + "name": "services-development-website___ja", + "path": "/ja/services/development/website", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/services/development/website.vue", + "name": "services-development-website___fr", + "path": "/fr/offres/developement/site-web", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/services/index.vue", + "name": "services___en", + "path": "/services", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/services/index.vue", + "name": "services___ja", + "path": "/ja/services", + }, + { + "children": [], + "file": "/path/to/nuxt-app/pages/services/index.vue", + "name": "services___fr", + "path": "/fr/offres", + }, +] +`; + exports[`Page components > JavaScript 1`] = ` [ { diff --git a/test/pages/custom_route.test.ts b/test/pages/custom_route.test.ts index 56328695c..253120001 100644 --- a/test/pages/custom_route.test.ts +++ b/test/pages/custom_route.test.ts @@ -136,6 +136,138 @@ describe.each([ }) }) +describe.each([ + { + case: 'simple', + options: getNuxtOptions( + { + about: { + en: '/about-us', + fr: '/a-propos', + es: '/sobre' + } + }, + 'named-config' + ), + pages: [ + { + name: 'about', + path: '/about', + file: '/path/to/nuxt-app/pages/about.vue', + children: [] + } + ] + }, + { + case: 'the part of URL', + options: getNuxtOptions( + { + about: { + fr: '/a-propos' + }, + services: { + fr: '/offres' + }, + 'services-development': { + fr: '/offres/developement' + }, + 'services-development-app': { + fr: '/offres/developement/app' + }, + 'services-development-website': { + fr: '/offres/developement/site-web' + }, + 'services-coaching': { + fr: '/offres/formation' + } + }, + 'named-config' + ), + pages: [ + { + name: 'about', + path: '/about', + file: '/path/to/nuxt-app/pages/about.vue', + children: [] + }, + { + name: 'services-coaching', + path: '/services/coaching', + file: '/path/to/nuxt-app/pages/services/coaching.vue', + children: [] + }, + { + name: 'services-development-app', + path: '/services/development/app', + file: '/path/to/nuxt-app/pages/services/development/app.vue', + children: [] + }, + { + name: 'services-development', + path: '/services/development', + file: '/path/to/nuxt-app/pages/services/development/index.vue', + children: [] + }, + { + name: 'services-development-website', + path: '/services/development/website', + file: '/path/to/nuxt-app/pages/services/development/website.vue', + children: [] + }, + { + name: 'services', + path: '/services', + file: '/path/to/nuxt-app/pages/services/index.vue', + children: [] + } + ] + }, + { + case: 'dynamic parameters', + options: getNuxtOptions( + { + 'blog-date-slug': { + ja: '/blog/tech/:date()/:slug()' + } + }, + 'named-config' + ), + pages: [ + { + name: 'blog-date-slug', + path: '/blog/:date()/:slug()', + file: '/path/to/nuxt-app/pages/blog/[date]/[slug].vue', + children: [] + } + ] + } +])('Module named configuration', ({ case: _case, options, pages }) => { + test(_case, async () => { + console.log(' -------------------------------------------------') + console.log('file: custom_route.test.ts:243 ~ options:', options) + console.log(' -------------------------------------------------') + vi.spyOn(fs, 'readFileSync').mockReturnValue('') + + const srcDir = '/path/to/nuxt-app' + const pagesDir = 'pages' + const ctx: NuxtPageAnalyzeContext = { + stack: [], + srcDir, + pagesDir, + pages: new Map() + } + + analyzeNuxtPages(ctx, pages) + const localizedPages = localizeRoutes(pages, { + ...options, + includeUprefixedFallback: false, + optionsResolver: getRouteOptionsResolver(ctx, options as Required) + } as Parameters[1]) + + expect(localizedPages).toMatchSnapshot() + }) +}) + describe.each([ { case: 'simple',