Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 2 additions & 1 deletion build.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
externals: ['node:fs', 'node:url', 'webpack', '@babel/parser']
externals: ['node:fs', 'node:url', 'webpack', '@babel/parser', 'unplugin-vue-router', 'unplugin-vue-router/options'],
failOnWarn: false
})
69 changes: 69 additions & 0 deletions src/gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,75 @@ declare module '#app' {
}
}

declare module 'vue-router' {
import type { RouteNamedMapI18n } from 'vue-router/auto-routes'

export interface TypesConfig {
RouteNamedMapI18n: RouteNamedMapI18n
}

export type RouteMapI18n =
TypesConfig extends Record<'RouteNamedMapI18n', infer RouteNamedMap> ? RouteNamedMap : RouteMapGeneric

export type RouteLocationRawI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationAsStringI18n | RouteLocationAsRelativeGeneric | RouteLocationAsPathGeneric
:
| _LiteralUnion<RouteLocationAsStringTypedList<RouteMapI18n>[Name], string>
| RouteLocationAsRelativeTypedList<RouteMapI18n>[Name]

export type RouteLocationResolved<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationResolvedGeneric
: RouteLocationResolvedTypedList<RouteMapI18n>[Name]

export interface RouteLocationNormalizedLoadedTypedI18n<
RouteMapI18n extends RouteMapGeneric = RouteMapGeneric,
Name extends keyof RouteMapI18n = keyof RouteMapI18n
> extends RouteLocationNormalizedLoadedGeneric {
name: Extract<Name, string | symbol>
params: RouteMapI18n[Name]['params']
}
export type RouteLocationNormalizedLoadedTypedListI18n<RouteMapOriginal extends RouteMapGeneric = RouteMapGeneric> = {
[N in keyof RouteMapOriginal]: RouteLocationNormalizedLoadedTypedI18n<RouteMapOriginal, N>
}
export type RouteLocationNormalizedLoadedI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationNormalizedLoadedGeneric
: RouteLocationNormalizedLoadedTypedListI18n<RouteMapI18n>[Name]

type _LiteralUnion<LiteralType, BaseType extends string = string> = LiteralType | (BaseType & Record<never, never>)

export type RouteLocationAsStringI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? string
: _LiteralUnion<RouteLocationAsStringTypedList<RouteMapI18n>[Name], string>

export type RouteLocationAsStringI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? string
: _LiteralUnion<RouteLocationAsStringTypedList<RouteMapI18n>[Name], string>

export type RouteLocationAsRelativeI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationAsRelativeGeneric
: RouteLocationAsRelativeTypedList<RouteMapI18n>[Name]

export type RouteLocationAsPathI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n ? RouteLocationAsPathGeneric : RouteLocationAsPathTypedList<RouteMapI18n>[Name]

/**
* Helper to generate a type safe version of the {@link RouteLocationAsRelative} type.
*/
export interface RouteLocationAsRelativeTyped<
RouteMapI18n extends RouteMapGeneric = RouteMapGeneric,
Name extends keyof RouteMapI18n = keyof RouteMapI18n
> extends RouteLocationAsRelativeGeneric {
name?: Extract<Name, string | symbol>
params?: RouteMapI18n[Name]['paramsRaw']
}
}

${(options.experimental?.autoImportTranslationFunctions && globalTranslationTypes) || ''}

export {}`
Expand Down
72 changes: 72 additions & 0 deletions src/internal-global-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,78 @@ declare module '#app' {
}
}

// This needs to be generated, or else `RouteMapI18n` will fall back to `RouteMapGeneric`
// declare module 'vue-router' {
// import type { RouteNamedMapI18n } from 'vue-router/auto-routes'

// export interface TypesConfig {
// RouteNamedMapI18n: RouteNamedMapI18n
// }
// }

declare module 'vue-router' {
export type RouteMapI18n =
TypesConfig extends Record<'RouteNamedMapI18n', infer RouteNamedMap> ? RouteNamedMap : RouteMapGeneric

export type RouteLocationRawI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationAsStringI18n | RouteLocationAsRelativeGeneric | RouteLocationAsPathGeneric
:
| _LiteralUnion<RouteLocationAsStringTypedList<RouteMapI18n>[Name], string>
| RouteLocationAsRelativeTypedList<RouteMapI18n>[Name]

export type RouteLocationResolvedI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationResolvedGeneric
: RouteLocationResolvedTypedList<RouteMapI18n>[Name]

export interface RouteLocationNormalizedLoadedTypedI18n<
RouteMapI18n extends RouteMapGeneric = RouteMapGeneric,
Name extends keyof RouteMapI18n = keyof RouteMapI18n
> extends RouteLocationNormalizedLoadedGeneric {
name: Extract<Name, string | symbol>
params: RouteMapI18n[Name]['params']
}
export type RouteLocationNormalizedLoadedTypedListI18n<RouteMapOriginal extends RouteMapGeneric = RouteMapGeneric> = {
[N in keyof RouteMapOriginal]: RouteLocationNormalizedLoadedTypedI18n<RouteMapOriginal, N>
}
export type RouteLocationNormalizedLoadedI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationNormalizedLoadedGeneric
: RouteLocationNormalizedLoadedTypedListI18n<RouteMapI18n>[Name]

type _LiteralUnion<LiteralType, BaseType extends string = string> = LiteralType | (BaseType & Record<never, never>)

export type RouteLocationAsStringI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? string
: _LiteralUnion<RouteLocationAsStringTypedList<RouteMapI18n>[Name], string>

export type RouteLocationAsStringI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? string
: _LiteralUnion<RouteLocationAsStringTypedList<RouteMapI18n>[Name], string>

export type RouteLocationAsRelativeI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n
? RouteLocationAsRelativeGeneric
: RouteLocationAsRelativeTypedList<RouteMapI18n>[Name]

export type RouteLocationAsPathI18n<Name extends keyof RouteMapI18n = keyof RouteMapI18n> =
RouteMapGeneric extends RouteMapI18n ? RouteLocationAsPathGeneric : RouteLocationAsPathTypedList<RouteMapI18n>[Name]

/**
* Helper to generate a type safe version of the {@link RouteLocationAsRelative} type.
*/
export interface RouteLocationAsRelativeTyped<
RouteMapI18n extends RouteMapGeneric = RouteMapGeneric,
Name extends keyof RouteMapI18n = keyof RouteMapI18n
> extends RouteLocationAsRelativeGeneric {
name?: Extract<Name, string | symbol>
params?: RouteMapI18n[Name]['paramsRaw']
}
}

declare global {
var $t: Composer['t']
var $rt: Composer['rt']
Expand Down
4 changes: 2 additions & 2 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export * from './types'

const debug = createDebug('@nuxtjs/i18n:module')

export default defineNuxtModule<NuxtI18nOptions>({
export default defineNuxtModule<Omit<NuxtI18nOptions, 'locales'> & { locales?: string[] | LocaleObject<string>[] }>({
meta: {
name: NUXT_I18N_MODULE_ID,
configKey: 'i18n',
Expand Down Expand Up @@ -184,7 +184,7 @@ export default defineNuxtModule<NuxtI18nOptions>({
*/

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

/**
Expand Down
118 changes: 112 additions & 6 deletions src/pages.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import createDebug from 'debug'
import { extendPages } from '@nuxt/kit'
import { addTemplate, extendPages } from '@nuxt/kit'
import { isString } from '@intlify/shared'
import { parse as parseSFC, compileScript } from '@vue/compiler-sfc'
import { walk } from 'estree-walker'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
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 { resolve, parse as parsePath, dirname } from 'pathe'
import { NUXT_I18N_COMPOSABLE_DEFINE_ROUTE } from './constants'
import { createRoutesContext } from 'unplugin-vue-router'
import { resolveOptions } from 'unplugin-vue-router/options'

import type { Nuxt, NuxtPage } from '@nuxt/schema'
import type { NuxtI18nOptions, CustomRoutePages, ComputedRouteOptions, RouteOptionsResolver } from './types'
import type { Node, ObjectExpression, ArrayExpression } from '@babel/types'

import { type Node, type ObjectExpression, type ArrayExpression } from '@babel/types'
import type { EditableTreeNode, Options as TypedRouterOptions } from 'unplugin-vue-router'
const debug = createDebug('@nuxtjs/i18n:pages')

export type AnalyzedNuxtPageMeta = {
Expand All @@ -34,7 +37,13 @@ export type NuxtPageAnalyzeContext = {
pages: Map<NuxtPage, AnalyzedNuxtPageMeta>
}

export function setupPages(options: Required<NuxtI18nOptions>, nuxt: Nuxt) {
/**
* Router type generation from Nuxt repo
* https://github.com/nuxt/nuxt/blob/48a8b18083ecfc5b9514d6683a2958e7a1a41e13/packages/nuxt/src/pages/module.ts#L160
*/
export async function setupPages(options: Required<NuxtI18nOptions>, nuxt: Nuxt) {
const useExperimentalTypedPages = nuxt.options.experimental.typedPages

let includeUnprefixedFallback = nuxt.options.ssr === false
nuxt.hook('nitro:init', () => {
debug('enable includeUprefixedFallback')
Expand All @@ -45,7 +54,88 @@ export function setupPages(options: Required<NuxtI18nOptions>, nuxt: Nuxt) {
const srcDir = nuxt.options.srcDir
debug(`pagesDir: ${pagesDir}, srcDir: ${srcDir}, trailingSlash: ${options.trailingSlash}`)

extendPages(pages => {
const typedRouterDtsFile = './types/typed-router-i18n.d.ts'
const dtsFile = resolve(nuxt.options.buildDir, typedRouterDtsFile)
function createTypedRouterContext(pages: NuxtPage[]) {
const typedRouteroptions: TypedRouterOptions = {
routesFolder: [],
dts: dtsFile,
logs: nuxt.options.debug,
// eslint-disable-next-line @typescript-eslint/require-await
async beforeWriteFiles(rootPage) {
rootPage.children.forEach(child => child.delete())
function addPage(parent: EditableTreeNode, page: NuxtPage) {
// @ts-expect-error TODO: either fix types upstream or figure out another
// way to add a route without a file, which must be possible
const route = parent.insert(page.path, page.file)
if (page.meta) {
route.addToMeta(page.meta)
}
if (page.alias) {
route.addAlias(page.alias)
}
if (page.name) {
route.name = page.name
}
// TODO: implement redirect support
// if (page.redirect) {}
if (page.children) {
page.children.forEach(child => addPage(route, child))
}
}

for (const page of pages) {
addPage(rootPage, page)
}
}
}

const context = createRoutesContext(resolveOptions(typedRouteroptions))
return context
}

if (useExperimentalTypedPages) {
// Augment `vue-router`
addTemplate({
filename: resolve(nuxt.options.buildDir, './types/i18n-generated-route-types.d.ts'),
getContents: () => {
return `// Generated by @nuxtjs/i18n
declare module 'vue-router' {
import type { RouteNamedMapI18n } from 'vue-router/auto-routes'

export interface TypesConfig {
RouteNamedMapI18n: RouteNamedMapI18n
}
}

export {}`
}
})

nuxt.hook('prepare:types', ({ references }) => {
// This file will be generated by unplugin-vue-router
references.push({ path: typedRouterDtsFile })
references.push({ types: './types/i18n-generated-route-types.d.ts' })
})
await mkdir(dirname(dtsFile), { recursive: true })

await createTypedRouterContext(nuxt.apps.default?.pages ?? []).scanPages(false)

// Not sure why Nuxt does something like this

// if (nuxt.options._prepare || !nuxt.options.dev) {
// // TODO: could we generate this from context instead?
// const dts = await readFile(dtsFile, 'utf-8')
// addTemplate({
// filename: 'types/typed-router-i18n.d.ts',
// getContents: () => {
// return dts.replace('interface RouteNamedMap', 'interface RouteNamedMapI18n')
// }
// })
// }
}

extendPages(async pages => {
debug('pages making ...', pages)
const ctx: NuxtPageAnalyzeContext = {
stack: [],
Expand All @@ -58,6 +148,22 @@ export function setupPages(options: Required<NuxtI18nOptions>, nuxt: Nuxt) {
const analyzer = (pageDirOverride: string) => analyzeNuxtPages(ctx, pages, pageDirOverride)
mergeLayerPages(analyzer, nuxt)

if (useExperimentalTypedPages) {
const context = createTypedRouterContext(pages)

// Wrap `scanPages`, rename interface
const originalScanPages = context.scanPages.bind(context)
context.scanPages = async function (watchers = true) {
await originalScanPages(watchers)
const f = await readFile(resolve(nuxt.options.buildDir, typedRouterDtsFile), 'utf-8')
await writeFile(
resolve(nuxt.options.buildDir, typedRouterDtsFile),
f.replace('interface RouteNamedMap', 'interface RouteNamedMapI18n')
)
}
await context.scanPages(false)
}

const localizedPages = localizeRoutes(pages, {
...options,
includeUnprefixedFallback,
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/components/NuxtLinkLocale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { hasProtocol } from 'ufo'

import type { PropType } from 'vue'
import type { NuxtLinkProps } from 'nuxt/app'
import type { RouteLocationRawI18n } from 'vue-router'

const NuxtLinkLocale = defineNuxtLink({ componentName: 'NuxtLinkLocale' })

export default defineComponent<NuxtLinkProps & { locale?: Locale }>({
export default defineComponent<Omit<NuxtLinkProps, 'to'> & { to?: RouteLocationRawI18n; locale?: Locale }>({
name: 'NuxtLinkLocale',
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- FIXME
props: {
Expand Down
Loading