Skip to content

Commit

Permalink
refactor: integrate localizeRoutes from vue-i18n-routing (nuxt-mo…
Browse files Browse the repository at this point in the history
…dules#2625)

* refactor: integrate `localizeRoutes` from `vue-i18n-routing`

* test: integrate `localizeRoutes` tests

* refactor: integrate localizeRoutes from vue-i18n-routing (nuxt-modules#28)
  • Loading branch information
BobbieGoede authored Dec 21, 2023
1 parent 7270c9c commit 69345ab
Show file tree
Hide file tree
Showing 10 changed files with 750 additions and 64 deletions.
2 changes: 1 addition & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export default defineNuxtModule<NuxtI18nOptions>({
*/

if (options.strategy !== 'no_prefix' && localeCodes.length) {
await setupPages(options, nuxt, { trailingSlash: options.trailingSlash })
await setupPages(options, nuxt)
}

/**
Expand Down
56 changes: 17 additions & 39 deletions src/pages.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import createDebug from 'debug'
import { extendPages } from '@nuxt/kit'
import { localizeRoutes, DefaultLocalizeRoutesPrefixable } from 'vue-i18n-routing'
import { isString } from '@intlify/shared'
import { parse as parseSFC, compileScript } from '@vue/compiler-sfc'
import { walk } from 'estree-walker'
import MagicString from 'magic-string'
import { formatMessage, getRoutePath, parseSegment, readFileSync } from './utils'
import { localizeRoutes } from './routing'
import { mergeLayerPages } from './layers'
import { resolve, parse as parsePath } from 'pathe'
import { NUXT_I18N_COMPOSABLE_DEFINE_ROUTE } from './constants'

import type { Nuxt, NuxtPage } from '@nuxt/schema'
import type {
I18nRoute,
RouteOptionsResolver,
ComputedRouteOptions,
LocalizeRoutesPrefixableOptions
} from 'vue-i18n-routing'
import type { NuxtI18nOptions, CustomRoutePages } from './types'
import type { ComputedRouteOptions, RouteOptionsResolver } from './routing'
import type { Node, ObjectExpression, ArrayExpression } from '@babel/types'

const debug = createDebug('@nuxtjs/i18n:pages')
Expand All @@ -34,29 +29,16 @@ export type NuxtPageAnalyzeContext = {
pages: Map<NuxtPage, AnalyzedNuxtPageMeta>
}

export function setupPages(
options: Required<NuxtI18nOptions>,
nuxt: Nuxt,
additionalOptions: { trailingSlash?: boolean } = {
trailingSlash: false
}
) {
// override prefixable path for localized target routes
function localizeRoutesPrefixable(opts: LocalizeRoutesPrefixableOptions): boolean {
// no prefix if app uses different locale domains
return !options.differentDomains && DefaultLocalizeRoutesPrefixable(opts)
}

let includeUprefixedFallback = nuxt.options.ssr === false
export function setupPages(options: Required<NuxtI18nOptions>, nuxt: Nuxt) {
let includeUnprefixedFallback = nuxt.options.ssr === false
nuxt.hook('nitro:init', () => {
debug('enable includeUprefixedFallback')
includeUprefixedFallback = options.strategy !== 'prefix'
includeUnprefixedFallback = options.strategy !== 'prefix'
})

const pagesDir = nuxt.options.dir && nuxt.options.dir.pages ? nuxt.options.dir.pages : 'pages'
const srcDir = nuxt.options.srcDir
const { trailingSlash } = additionalOptions
debug(`pagesDir: ${pagesDir}, srcDir: ${srcDir}, tailingSlash: ${trailingSlash}`)
debug(`pagesDir: ${pagesDir}, srcDir: ${srcDir}, trailingSlash: ${options.trailingSlash}`)

extendPages(pages => {
debug('pages making ...', pages)
Expand All @@ -71,11 +53,9 @@ export function setupPages(
const analyzer = (pageDirOverride: string) => analyzeNuxtPages(ctx, pages, pageDirOverride)
mergeLayerPages(analyzer, nuxt)

// @ts-expect-error Nuxt allows any valid redirect object, not just strings
const localizedPages = localizeRoutes(pages, {
...options,
includeUprefixedFallback,
localizeRoutesPrefixable,
includeUnprefixedFallback,
optionsResolver: getRouteOptionsResolver(ctx, options)
})
pages.splice(0, pages.length)
Expand Down Expand Up @@ -130,7 +110,7 @@ export function getRouteOptionsResolver(
const useConfig = customRoutes === 'config'
debug('getRouteOptionsResolver useConfig', useConfig)

return (route, localeCodes): ComputedRouteOptions | null => {
return (route, localeCodes): ComputedRouteOptions | undefined => {
const ret = useConfig
? getRouteOptionsFromPages(ctx, route, localeCodes, pages, defaultLocale)
: getRouteOptionsFromComponent(route, localeCodes)
Expand All @@ -148,7 +128,7 @@ function resolveRoutePath(path: string): string {

function getRouteOptionsFromPages(
ctx: NuxtPageAnalyzeContext,
route: I18nRoute,
route: NuxtPage,
localeCodes: string[],
pages: CustomRoutePages,
defaultLocale: string
Expand All @@ -173,7 +153,7 @@ function getRouteOptionsFromPages(

// routing disabled
if (pageOptions === false) {
return null
return undefined
}

// skip if no page options defined
Expand Down Expand Up @@ -203,13 +183,13 @@ function getRouteOptionsFromPages(
return options
}

function getRouteOptionsFromComponent(route: I18nRoute, localeCodes: string[]) {
function getRouteOptionsFromComponent(route: NuxtPage, localeCodes: string[]) {
debug('getRouteOptionsFromComponent', route)
const file = route.component || route.file
const file = route.file

// localize disabled if no file (vite) or component (webpack)
if (!isString(file)) {
return null
return undefined
}

const options: ComputedRouteOptions = {
Expand All @@ -226,17 +206,15 @@ function getRouteOptionsFromComponent(route: I18nRoute, localeCodes: string[]) {

// localize disabled
if (componentOptions === false) {
return null
return undefined
}

options.locales = componentOptions.locales || localeCodes

// construct paths object
const locales = Object.keys(componentOptions.paths || {})
for (const locale of locales) {
const customLocalePath = componentOptions.paths[locale]
if (isString(customLocalePath)) {
options.paths[locale] = resolveRoutePath(customLocalePath)
for (const [locale, path] of Object.entries(componentOptions.paths ?? {})) {
if (isString(path)) {
options.paths[locale] = resolveRoutePath(path)
}
}

Expand Down
186 changes: 186 additions & 0 deletions src/routing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { getNormalizedLocales } from './utils'

import type { Locale } from 'vue-i18n'
import type { NuxtPage } from '@nuxt/schema'
import type { MarkRequired, MarkOptional } from 'ts-essentials'
import type { NuxtI18nOptions } from './types'

const join = (...args: (string | undefined)[]) => args.filter(Boolean).join('')

/**
* Options to compute route localizing
*
* @remarks
* The route options that is compute the route to be localized on {@link localizeRoutes}
*
* @public
*/
export declare interface ComputedRouteOptions {
locales: readonly string[]
paths: Record<string, string>
}

/**
* Resolver for route localizing options
*
* @public
*/
export declare type RouteOptionsResolver = (route: NuxtPage, localeCodes: string[]) => ComputedRouteOptions | undefined

/**
* Localize route path prefix judgment options used in {@link LocalizeRoutesPrefixable}
*
* @public
*/
export interface PrefixLocalizedRouteOptions {
/**
* Current locale
*/
locale: Locale
/**
* Default locale
*/
defaultLocale?: Locale | undefined
/**
* The parent route of the route to be resolved
*/
parent: NuxtPage | undefined
/**
* The path of route
*/
path: string
}
function prefixLocalizedRoute(
localizeOptions: PrefixLocalizedRouteOptions,
options: LocalizeRoutesParams,
extra = false
): boolean {
const isDefaultLocale = localizeOptions.locale === (options.defaultLocale ?? '')
const isChildWithRelativePath = localizeOptions.parent != null && !localizeOptions.path.startsWith('/')

// no need to add prefix if child's path is relative
return (
!extra &&
!options.differentDomains &&
!isChildWithRelativePath &&
// skip default locale if strategy is 'prefix_except_default'
!(isDefaultLocale && options.strategy === 'prefix_except_default')
)
}

function adjustRoutePathForTrailingSlash(localized: LocalizedRoute, trailingSlash: boolean) {
const isChildWithRelativePath = localized.parent != null && !localized.path.startsWith('/')
return localized.path.replace(/\/+$/, '') + (trailingSlash ? '/' : '') || (isChildWithRelativePath ? '' : '/')
}

export type LocalizeRoutesParams = MarkRequired<
NuxtI18nOptions,
'strategy' | 'locales' | 'routesNameSeparator' | 'trailingSlash' | 'defaultLocaleRouteNameSuffix'
> & {
includeUnprefixedFallback?: boolean
optionsResolver?: RouteOptionsResolver
}

type LocalizedRoute = NuxtPage & { locale: Locale; parent: NuxtPage | undefined }
type LocalizeRouteParams = {
/**
* locales to use for localization
*/
locales: string[]
/**
* parent route
*/
parent?: NuxtPage
/**
* indicates whether this is a default route for 'prefix_and_default' strategy
*/
extra?: boolean
}

/**
* Localize routes
*
* @param routes - Some routes
* @param options - An options
*
* @returns Localized routes
*
* @public
*/
export function localizeRoutes(routes: NuxtPage[], options: LocalizeRoutesParams): NuxtPage[] {
if (options.strategy === 'no_prefix') {
return routes
}

function localizeRoute(route: NuxtPage, { locales = [], parent, extra = false }: LocalizeRouteParams): NuxtPage[] {
// skip route localization
if (route.redirect && !route.file) {
return [route]
}

// resolve with route (page) options
const routeOptions = options.optionsResolver?.(route, locales)
if (options.optionsResolver != null && routeOptions == null) {
return [route]
}

// component specific options
const componentOptions: ComputedRouteOptions = {
// filter locales to prevent child routes from being localized even though they are disabled in the configuration.
locales: locales.filter(locale => (routeOptions?.locales ?? locales).includes(locale)),
paths: {},
...routeOptions
}

const localizedRoutes: (LocalizedRoute | NuxtPage)[] = []
for (const locale of componentOptions.locales) {
const localized: LocalizedRoute = { ...route, locale, parent }
const isDefaultLocale = locale === options.defaultLocale
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)
}

// localize name if set
localized.name &&= join(...nameSegments)

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

// use custom path if found
localized.path = componentOptions.paths?.[locale] ?? localized.path

const localePrefixable = prefixLocalizedRoute(localized, options, extra)
if (localePrefixable) {
localized.path = join('/', locale, localized.path)

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

localized.path &&= adjustRoutePathForTrailingSlash(localized, options.trailingSlash)
localizedRoutes.push(localized)
}

// remove properties used for localization process
return localizedRoutes.flatMap((x: MarkOptional<LocalizedRoute, 'parent' | 'locale'>) => {
delete x.parent
delete x.locale
return x
})
}

return routes.flatMap(route =>
localizeRoute(route, { locales: getNormalizedLocales(options.locales).map(x => x.code) })
)
}
Loading

0 comments on commit 69345ab

Please sign in to comment.