Skip to content

Commit 70a7227

Browse files
committed
fix: implement localized routes penality
1 parent fd8bcbd commit 70a7227

File tree

3 files changed

+46
-3
lines changed

3 files changed

+46
-3
lines changed

packages/nextjs/src/client/routing/parameterization.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ function getRouteSpecificity(routePath: string): number {
3737
// Static segments add 0 to score as they are most specific
3838
}
3939

40+
if (segments.length > 0) {
41+
// Add a small penalty based on inverse of segment count
42+
// This ensures that routes with more segments are preferred
43+
// e.g., '/:locale/foo' is more specific than '/:locale'
44+
// We use a small value (1 / segments.length) so it doesn't override the main scoring
45+
// but breaks ties between routes with the same number of dynamic segments
46+
const segmentCountPenalty = 1 / segments.length;
47+
score += segmentCountPenalty;
48+
}
49+
4050
return score;
4151
}
4252

@@ -134,6 +144,23 @@ function findMatchingRoutes(
134144
}
135145
}
136146

147+
// Try matching with optional prefix segments (for i18n routing patterns)
148+
// This handles cases like '/foo' matching '/:locale/foo' when using next-intl with localePrefix: "as-needed"
149+
// We do this regardless of whether we found direct matches, as we want the most specific match
150+
if (!route.startsWith('/:')) {
151+
for (const dynamicRoute of dynamicRoutes) {
152+
if (dynamicRoute.hasOptionalPrefix && dynamicRoute.regex) {
153+
// Prepend a placeholder segment to simulate the optional prefix
154+
// e.g., '/foo' becomes '/PLACEHOLDER/foo' to match '/:locale/foo'
155+
const routeWithPrefix = `/SENTRY_OPTIONAL_PREFIX${route}`;
156+
const regex = getCompiledRegex(dynamicRoute.regex);
157+
if (regex?.test(routeWithPrefix)) {
158+
matches.push(dynamicRoute.path);
159+
}
160+
}
161+
}
162+
}
163+
137164
return matches;
138165
}
139166

packages/nextjs/src/config/manifest/createRouteManifest.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ function getDynamicRouteSegment(name: string): string {
4747
return `:${name.slice(1, -1)}`;
4848
}
4949

50-
function buildRegexForDynamicRoute(routePath: string): { regex: string; paramNames: string[] } {
50+
function buildRegexForDynamicRoute(routePath: string): {
51+
regex: string;
52+
paramNames: string[];
53+
hasOptionalPrefix: boolean;
54+
} {
5155
const segments = routePath.split('/').filter(Boolean);
5256
const regexSegments: string[] = [];
5357
const paramNames: string[] = [];
@@ -95,7 +99,13 @@ function buildRegexForDynamicRoute(routePath: string): { regex: string; paramNam
9599
pattern = `^/${regexSegments.join('/')}$`;
96100
}
97101

98-
return { regex: pattern, paramNames };
102+
// Detect if the first parameter is a common i18n prefix segment
103+
// Common patterns: locale, lang, language
104+
const firstParam = paramNames[0];
105+
const hasOptionalPrefix =
106+
firstParam !== undefined && (firstParam === 'locale' || firstParam === 'lang' || firstParam === 'language');
107+
108+
return { regex: pattern, paramNames, hasOptionalPrefix };
99109
}
100110

101111
function scanAppDirectory(
@@ -116,11 +126,12 @@ function scanAppDirectory(
116126
const isDynamic = routePath.includes(':');
117127

118128
if (isDynamic) {
119-
const { regex, paramNames } = buildRegexForDynamicRoute(routePath);
129+
const { regex, paramNames, hasOptionalPrefix } = buildRegexForDynamicRoute(routePath);
120130
dynamicRoutes.push({
121131
path: routePath,
122132
regex,
123133
paramNames,
134+
hasOptionalPrefix,
124135
});
125136
} else {
126137
staticRoutes.push({

packages/nextjs/src/config/manifest/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ export type RouteInfo = {
1414
* (Optional) The names of dynamic parameters in the route
1515
*/
1616
paramNames?: string[];
17+
/**
18+
* (Optional) Indicates if the first segment is an optional prefix (e.g., for i18n routing)
19+
* When true, routes like '/foo' should match '/:locale/foo' patterns
20+
*/
21+
hasOptionalPrefix?: boolean;
1722
};
1823

1924
/**

0 commit comments

Comments
 (0)