Skip to content

Commit 9068492

Browse files
committed
handle overlapping custom prefixes
1 parent f69a10f commit 9068492

File tree

4 files changed

+96
-16
lines changed

4 files changed

+96
-16
lines changed

packages/next-intl/src/middleware/middleware.test.tsx

+75-13
Original file line numberDiff line numberDiff line change
@@ -3194,20 +3194,73 @@ describe('domain-based routing', () => {
31943194
describe('custom prefixes with pathnames', () => {
31953195
const middlewareWithPrefixes = createMiddleware({
31963196
defaultLocale: 'en',
3197-
locales: ['en', 'en-gb'],
3197+
locales: ['en', 'en-gb', 'sv-SE', 'en-SE', 'no-NO', 'en-NO'],
31983198
localePrefix: {
31993199
mode: 'as-needed',
32003200
prefixes: {
3201-
'en-gb': '/uk'
3201+
'en-gb': '/uk',
3202+
'en-SE': '/en',
3203+
'en-NO': '/en'
32023204
}
32033205
},
32043206
pathnames: {
32053207
'/': '/',
32063208
'/about': {
32073209
en: '/about',
3208-
'en-gb': '/about'
3210+
'en-gb': '/about',
3211+
'en-SE': '/about',
3212+
'en-NO': '/about',
3213+
'sv-SE': '/about',
3214+
'no-NO': '/about'
32093215
}
3210-
} satisfies Pathnames<ReadonlyArray<'en' | 'en-gb'>>
3216+
} satisfies Pathnames<
3217+
ReadonlyArray<'en' | 'en-gb' | 'sv-SE' | 'en-SE' | 'no-NO' | 'en-NO'>
3218+
>,
3219+
domains: [
3220+
{
3221+
defaultLocale: 'en-gb',
3222+
domain: 'example.co.uk',
3223+
locales: ['en-gb']
3224+
},
3225+
{
3226+
defaultLocale: 'sv-SE',
3227+
domain: 'example.se',
3228+
locales: ['sv-SE', 'en-SE']
3229+
},
3230+
{
3231+
defaultLocale: 'no-NO',
3232+
domain: 'example.no',
3233+
locales: ['no-NO', 'en-NO']
3234+
},
3235+
{
3236+
defaultLocale: 'en',
3237+
domain: 'example.com',
3238+
locales: ['en']
3239+
}
3240+
]
3241+
});
3242+
3243+
it('serves requests for overlapping prefixes', () => {
3244+
middlewareWithPrefixes(
3245+
createMockRequest('/', undefined, 'http://example.com')
3246+
);
3247+
middlewareWithPrefixes(
3248+
createMockRequest('/en', undefined, 'http://example.no')
3249+
);
3250+
middlewareWithPrefixes(
3251+
createMockRequest('/en', undefined, 'http://example.se')
3252+
);
3253+
expect(MockedNextResponse.redirect).not.toHaveBeenCalled();
3254+
expect(MockedNextResponse.rewrite).toHaveBeenCalledTimes(3);
3255+
expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe(
3256+
'http://example.com/en'
3257+
);
3258+
expect(MockedNextResponse.rewrite.mock.calls[1][0].toString()).toBe(
3259+
'http://example.no/en-NO'
3260+
);
3261+
expect(MockedNextResponse.rewrite.mock.calls[2][0].toString()).toBe(
3262+
'http://example.se/en-SE'
3263+
);
32113264
});
32123265

32133266
it('serves requests for the default locale at the root', () => {
@@ -3257,24 +3310,33 @@ describe('domain-based routing', () => {
32573310

32583311
['/', '/uk'].forEach((pathname) => {
32593312
expect(getLinks(createMockRequest(pathname))).toEqual([
3260-
'<http://localhost:3000/>; rel="alternate"; hreflang="en"',
3261-
'<http://localhost:3000/uk>; rel="alternate"; hreflang="en-gb"',
3262-
'<http://localhost:3000/>; rel="alternate"; hreflang="x-default"'
3313+
'<http://example.com/>; rel="alternate"; hreflang="en"',
3314+
'<http://example.co.uk/>; rel="alternate"; hreflang="en-gb"',
3315+
'<http://example.se/>; rel="alternate"; hreflang="sv-SE"',
3316+
'<http://example.se/en>; rel="alternate"; hreflang="en-SE"',
3317+
'<http://example.no/>; rel="alternate"; hreflang="no-NO"',
3318+
'<http://example.no/en>; rel="alternate"; hreflang="en-NO"'
32633319
]);
32643320
});
32653321

32663322
['/about', '/uk/about'].forEach((pathname) => {
32673323
expect(getLinks(createMockRequest(pathname))).toEqual([
3268-
'<http://localhost:3000/about>; rel="alternate"; hreflang="en"',
3269-
'<http://localhost:3000/uk/about>; rel="alternate"; hreflang="en-gb"',
3270-
'<http://localhost:3000/about>; rel="alternate"; hreflang="x-default"'
3324+
'<http://example.com/about>; rel="alternate"; hreflang="en"',
3325+
'<http://example.co.uk/about>; rel="alternate"; hreflang="en-gb"',
3326+
'<http://example.se/about>; rel="alternate"; hreflang="sv-SE"',
3327+
'<http://example.se/en/about>; rel="alternate"; hreflang="en-SE"',
3328+
'<http://example.no/about>; rel="alternate"; hreflang="no-NO"',
3329+
'<http://example.no/en/about>; rel="alternate"; hreflang="en-NO"'
32713330
]);
32723331
});
32733332

32743333
expect(getLinks(createMockRequest('/unknown'))).toEqual([
3275-
'<http://localhost:3000/unknown>; rel="alternate"; hreflang="en"',
3276-
'<http://localhost:3000/uk/unknown>; rel="alternate"; hreflang="en-gb"',
3277-
'<http://localhost:3000/unknown>; rel="alternate"; hreflang="x-default"'
3334+
'<http://example.com/unknown>; rel="alternate"; hreflang="en"',
3335+
'<http://example.co.uk/unknown>; rel="alternate"; hreflang="en-gb"',
3336+
'<http://example.se/unknown>; rel="alternate"; hreflang="sv-SE"',
3337+
'<http://example.se/en/unknown>; rel="alternate"; hreflang="en-SE"',
3338+
'<http://example.no/unknown>; rel="alternate"; hreflang="no-NO"',
3339+
'<http://example.no/en/unknown>; rel="alternate"; hreflang="en-NO"'
32783340
]);
32793341
});
32803342
});

packages/next-intl/src/middleware/middleware.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ export default function createMiddleware<
152152
const pathnameMatch = getPathnameMatch(
153153
externalPathname,
154154
resolvedRouting.locales,
155-
resolvedRouting.localePrefix
155+
resolvedRouting.localePrefix,
156+
domain
156157
);
157158
const hasLocalePrefix = pathnameMatch != null;
158159

packages/next-intl/src/middleware/resolveLocale.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@ function resolveLocaleFromDomain<
169169
const prefixLocale = getPathnameMatch(
170170
pathname,
171171
routing.locales,
172-
routing.localePrefix
172+
routing.localePrefix,
173+
domain
173174
)?.locale;
174175
if (prefixLocale) {
175176
if (isLocaleSupportedOnDomain(prefixLocale, domain)) {

packages/next-intl/src/middleware/utils.tsx

+17-1
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ export function getPathnameMatch<
159159
>(
160160
pathname: string,
161161
locales: AppLocales,
162-
localePrefix: LocalePrefixConfigVerbose<AppLocales, AppLocalePrefixMode>
162+
localePrefix: LocalePrefixConfigVerbose<AppLocales, AppLocalePrefixMode>,
163+
domain?: DomainConfig<AppLocales>
163164
):
164165
| {
165166
locale: AppLocales[number];
@@ -170,6 +171,21 @@ export function getPathnameMatch<
170171
| undefined {
171172
const localePrefixes = getLocalePrefixes(locales, localePrefix);
172173

174+
// Sort to prioritize domain locales
175+
if (domain) {
176+
localePrefixes.sort(([localeA], [localeB]) => {
177+
if (localeA === domain.defaultLocale) return -1;
178+
if (localeB === domain.defaultLocale) return 1;
179+
180+
const isLocaleAInDomain = domain.locales.includes(localeA);
181+
const isLocaleBInDomain = domain.locales.includes(localeB);
182+
if (isLocaleAInDomain && !isLocaleBInDomain) return -1;
183+
if (!isLocaleAInDomain && isLocaleBInDomain) return 1;
184+
185+
return 0;
186+
});
187+
}
188+
173189
for (const [locale, prefix] of localePrefixes) {
174190
let exact, matches;
175191
if (pathname === prefix || pathname.startsWith(prefix + '/')) {

0 commit comments

Comments
 (0)