diff --git a/packages/next-intl/src/middleware/utils.test.tsx b/packages/next-intl/src/middleware/utils.test.tsx index e2609114b..e322b5538 100644 --- a/packages/next-intl/src/middleware/utils.test.tsx +++ b/packages/next-intl/src/middleware/utils.test.tsx @@ -3,6 +3,7 @@ import { formatPathnameTemplate, getInternalTemplate, getNormalizedPathname, + getPathnameMatch, getRouteParams } from './utils'; @@ -168,3 +169,36 @@ describe('getInternalTemplate', () => { ]); }); }); + +describe('getPathnameMatch', () => { + it('prioritizes more specific custom prefixes for overlapping ones', () => { + const locales = ['de', 'de-at', 'de-at-x-test'] as const; + const localePrefix = { + mode: 'always', + prefixes: { + de: '/de', + 'de-at': '/de/at', + // Longer locale, shorter prefix + 'de-at-x-test': '/de/a' + } + } as const; + + expect(getPathnameMatch('/de/at/test', locales, localePrefix)).toEqual({ + locale: 'de-at', + prefix: '/de/at', + exact: true, + matchedPrefix: '/de/at' + }); + }); + + it('does not confuse unrelated parts of the pathname with a locale', () => { + expect( + getPathnameMatch('/de/ats', ['de', 'de-at'], {mode: 'always'}) + ).toEqual({ + locale: 'de', + prefix: '/de', + exact: true, + matchedPrefix: '/de' + }); + }); +}); diff --git a/packages/next-intl/src/middleware/utils.tsx b/packages/next-intl/src/middleware/utils.tsx index 26fda1b75..d70468166 100644 --- a/packages/next-intl/src/middleware/utils.tsx +++ b/packages/next-intl/src/middleware/utils.tsx @@ -151,6 +151,9 @@ export function getPathnameMatch( | undefined { const localePrefixes = getLocalePrefixes(locales, localePrefix); + // More specific ones first + localePrefixes.sort((a, b) => b[1].length - a[1].length); + for (const [locale, prefix] of localePrefixes) { let exact, matches; if (pathname === prefix || pathname.startsWith(prefix + '/')) {