From 72c1731892db6e7d0470cefcea2b1f22a5f37ce2 Mon Sep 17 00:00:00 2001 From: Jan Amann Date: Tue, 17 Sep 2024 11:12:18 +0200 Subject: [PATCH] fix: Handle overlapping custom locale prefixes correctly (#1343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consider this config: ```tsx export const routing = defineRouting({ locales: ["en-US", "es-US"], defaultLocale: "en-US", localePrefix: { mode: "always", prefixes: { "es-US": "/us/es", "en-US": "/us" }, } }); ``` … a pathname like `/us/es` would previously be incorrectly rewritten to `/en-US/es` internally. With this change, we now prioritize more specific prefixes in order to rewrite this request correctly to `/es-US`. Fixes #1329 --- .../next-intl/src/middleware/utils.test.tsx | 34 +++++++++++++++++++ packages/next-intl/src/middleware/utils.tsx | 3 ++ 2 files changed, 37 insertions(+) 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 + '/')) {