Skip to content

Commit d244b1f

Browse files
committed
feat(page-manager): add support for custom exclusion patterns in i18n
1 parent b171580 commit d244b1f

File tree

4 files changed

+86
-11
lines changed

4 files changed

+86
-11
lines changed

src/module.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export default defineNuxtModule<ModuleOptions>({
106106
return selectedForm.trim().replace('{count}', count.toString())
107107
},
108108
customRegexMatcher: undefined,
109+
excludePatterns: undefined,
109110
},
110111
async setup(options, nuxt) {
111112
const defaultLocale = process.env.DEFAULT_LOCALE ?? options.defaultLocale ?? 'en'
@@ -130,7 +131,7 @@ export default defineNuxtModule<ModuleOptions>({
130131
const rootDirs = nuxt.options._layers.map(layer => layer.config.rootDir).reverse()
131132

132133
const localeManager = new LocaleManager(options, rootDirs)
133-
const pageManager = new PageManager(localeManager.locales, defaultLocale, options.strategy!, options.globalLocaleRoutes, options.noPrefixRedirect!)
134+
const pageManager = new PageManager(localeManager.locales, defaultLocale, options.strategy!, options.globalLocaleRoutes, options.noPrefixRedirect!, options.excludePatterns)
134135

135136
addTemplate({
136137
filename: 'i18n.plural.mjs',
@@ -158,6 +159,7 @@ export default defineNuxtModule<ModuleOptions>({
158159
isSSG: isSSG,
159160
disablePageLocales: options.disablePageLocales ?? false,
160161
canonicalQueryWhitelist: options.canonicalQueryWhitelist ?? [],
162+
excludePatterns: options.excludePatterns ?? [],
161163
}
162164

163165
// if there is a customRegexMatcher set and all locales don't match the custom matcher, throw error
@@ -294,7 +296,7 @@ export default defineNuxtModule<ModuleOptions>({
294296
const fullPath = path.posix.normalize(`${parentPath}/${page.path}`) // Combine parent path and current path
295297

296298
// Skip internal paths
297-
if (isInternalPath(fullPath)) {
299+
if (isInternalPath(fullPath, options.excludePatterns)) {
298300
return
299301
}
300302

@@ -328,15 +330,15 @@ export default defineNuxtModule<ModuleOptions>({
328330
}
329331

330332
// Add localized path to array
331-
if (!isInternalPath(localizedPath)) {
333+
if (!isInternalPath(localizedPath, options.excludePatterns)) {
332334
prerenderRoutes.push(localizedPath)
333335
}
334336
}
335337
})
336338
}
337339
else {
338340
// If there's no dynamic locale segment in the path, just add it to the array
339-
if (!isInternalPath(fullPath)) {
341+
if (!isInternalPath(fullPath, options.excludePatterns)) {
340342
prerenderRoutes.push(fullPath)
341343
}
342344
}
@@ -514,7 +516,7 @@ export default defineNuxtModule<ModuleOptions>({
514516
// Remove internal paths before localization processing
515517
const routesToRemove: string[] = []
516518
routesSet.forEach((route) => {
517-
if (isInternalPath(route)) {
519+
if (isInternalPath(route, options.excludePatterns)) {
518520
routesToRemove.push(route)
519521
}
520522
})
@@ -525,7 +527,7 @@ export default defineNuxtModule<ModuleOptions>({
525527

526528
// Go through each existing route and add localized versions
527529
routesSet.forEach((route) => {
528-
if (!/\.[a-z0-9]+$/i.test(route) && !isInternalPath(route)) {
530+
if (!/\.[a-z0-9]+$/i.test(route) && !isInternalPath(route, options.excludePatterns)) {
529531
localeManager.locales!.forEach((locale) => {
530532
// For prefix and prefix_and_default strategies generate routes for defaultLocale too
531533
// For prefix_except_default strategy skip defaultLocale

src/page-manager.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ export class PageManager {
2929
activeLocaleCodes: string[]
3030
globalLocaleRoutes: Record<string, Record<string, string> | false | boolean>
3131
noPrefixRedirect: boolean
32+
excludePatterns: (string | RegExp)[] | undefined
3233

33-
constructor(locales: Locale[], defaultLocaleCode: string, strategy: Strategies, globalLocaleRoutes: GlobalLocaleRoutes, noPrefixRedirect: boolean) {
34+
constructor(locales: Locale[], defaultLocaleCode: string, strategy: Strategies, globalLocaleRoutes: GlobalLocaleRoutes, noPrefixRedirect: boolean, excludePatterns?: (string | RegExp)[]) {
3435
this.locales = locales
3536
this.defaultLocale = this.findLocaleByCode(defaultLocaleCode) || { code: defaultLocaleCode }
3637
this.strategy = strategy
3738
this.noPrefixRedirect = noPrefixRedirect
39+
this.excludePatterns = excludePatterns
3840
this.activeLocaleCodes = this.computeActiveLocaleCodes()
3941
this.globalLocaleRoutes = globalLocaleRoutes || {}
4042
}
@@ -62,7 +64,7 @@ export class PageManager {
6264

6365
for (const page of [...pages]) {
6466
// Skip internal paths during page processing
65-
if (page.path && isInternalPath(page.path)) {
67+
if (page.path && isInternalPath(page.path, this.excludePatterns)) {
6668
continue
6769
}
6870

@@ -98,7 +100,7 @@ export class PageManager {
98100
const pageName = page.name ?? ''
99101

100102
// Skip removal for internal paths
101-
if (isInternalPath(pagePath)) continue
103+
if (isInternalPath(pagePath, this.excludePatterns)) continue
102104

103105
if (this.globalLocaleRoutes[pageName] === false) continue
104106

src/runtime/components/locale-redirect.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,28 @@
33
</template>
44

55
<script setup>
6-
import { useRoute, useI18n, createError, navigateTo } from '#imports'
6+
import { useRoute, useI18n, createError, navigateTo, useRuntimeConfig } from '#imports'
7+
import { isInternalPath } from '../../utils'
78
89
const route = useRoute()
910
const { $getLocales, $defaultLocale } = useI18n()
11+
const config = useRuntimeConfig()
1012
1113
const locales = $getLocales().map(locale => locale.code)
1214
const defaultLocale = $defaultLocale() || 'en'
1315
const pathSegments = route.fullPath.split('/')
1416
const firstSegment = pathSegments[1]
1517
18+
// Check if this path should be excluded from i18n processing
19+
const excludePatterns = config.public.i18nConfig?.excludePatterns
20+
if (isInternalPath(route.fullPath, excludePatterns)) {
21+
// This is a static file or excluded path, let Nuxt handle it normally
22+
throw createError({
23+
statusCode: 404,
24+
statusMessage: 'Static file - should not be processed by i18n',
25+
})
26+
}
27+
1628
const generateRouteName = (segments) => {
1729
return segments
1830
.slice(1)

src/utils.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,66 @@ import path from 'node:path'
22
import type { NuxtPage } from '@nuxt/schema'
33
import type { Locale, LocaleCode } from 'nuxt-i18n-micro-types'
44

5-
export const isInternalPath = (p: string) => /(?:^|\/)__[^/]+/.test(p)
5+
/**
6+
* Default patterns for static files that should be excluded from i18n routing
7+
*/
8+
const DEFAULT_STATIC_PATTERNS = [
9+
/^\/sitemap.*\.xml$/,
10+
/^\/sitemap\.xml$/,
11+
/^\/robots\.txt$/,
12+
/^\/favicon\.ico$/,
13+
/^\/apple-touch-icon.*\.png$/,
14+
/^\/manifest\.json$/,
15+
/^\/sw\.js$/,
16+
/^\/workbox-.*\.js$/,
17+
/\.(xml|txt|ico|json|js|css|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$/,
18+
]
19+
20+
/**
21+
* Checks if a path should be excluded from i18n routing
22+
* @param path - The path to check
23+
* @param excludePatterns - Optional custom exclusion patterns
24+
* @returns true if the path should be excluded
25+
*/
26+
export const isInternalPath = (path: string, excludePatterns?: (string | RegExp | object)[]): boolean => {
27+
// Check internal Nuxt paths (existing behavior)
28+
if (/(?:^|\/)__[^/]+/.test(path)) {
29+
return true
30+
}
31+
32+
// Check default static file patterns
33+
for (const pattern of DEFAULT_STATIC_PATTERNS) {
34+
if (pattern.test(path)) {
35+
return true
36+
}
37+
}
38+
39+
// Check custom exclusion patterns
40+
if (excludePatterns) {
41+
for (const pattern of excludePatterns) {
42+
if (typeof pattern === 'string') {
43+
// Convert string to regex if it contains wildcards or is a simple match
44+
if (pattern.includes('*') || pattern.includes('?')) {
45+
const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'))
46+
if (regex.test(path)) {
47+
return true
48+
}
49+
}
50+
else if (path === pattern || path.startsWith(pattern)) {
51+
return true
52+
}
53+
}
54+
else if (pattern instanceof RegExp) {
55+
if (pattern.test(path)) {
56+
return true
57+
}
58+
}
59+
// Skip empty objects or other types
60+
}
61+
}
62+
63+
return false
64+
}
665

766
export function extractLocaleRoutes(content: string, filePath: string): Record<string, string> | null {
867
// Look for defineI18nRoute call (with or without dollar sign)

0 commit comments

Comments
 (0)