Skip to content

Commit

Permalink
fix: Handle optional params in catch-all segments correctly when usin…
Browse files Browse the repository at this point in the history
…g localized pathnames (#925)

Fixes #917

This works correctly now:

```
  "/items/[[...slug]]": {
    tr: "/ilanlar/[[...slug]]",
    en: "/items/[[...slug]]",
  },
```

… should match when `/tr/ilanlar` is called.
  • Loading branch information
amannn authored Mar 7, 2024
1 parent ec727c0 commit 8ba8b69
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 21 deletions.
13 changes: 6 additions & 7 deletions packages/next-intl/src/shared/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,12 @@ export function matchesPathname(

export function templateToRegex(template: string): RegExp {
const regexPattern = template
.replace(/\[([^\]]+)\]/g, (match) => {
if (match.startsWith('[...')) return '(.*)';
if (match.startsWith('[[...')) return '(.*)';
return '([^/]+)';
})
// Clean up regex match remainders from optional catchall ('[[...slug]]')
.replaceAll('(.*)]', '(.*)');
// Replace optional catchall ('[[...slug]]')
.replaceAll(/\[\[(\.\.\.[^\]]+)\]\]/g, '?(.*)')
// Replace catchall ('[...slug]')
.replaceAll(/\[(\.\.\.[^\]]+)\]/g, '(.+)')
// Replace regular parameter ('[slug]')
.replaceAll(/\[([^\]]+)\]/g, '([^/]+)');

return new RegExp(`^${regexPattern}$`);
}
39 changes: 25 additions & 14 deletions packages/next-intl/test/middleware/middleware.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,10 @@ describe('prefix-based routing', () => {
'/products/[...slug]': {
en: '/products/[...slug]',
de: '/produkte/[...slug]'
},
'/categories/[[...slug]]': {
en: '/categories/[[...slug]]',
de: '/kategorien/[[...slug]]'
}
} satisfies Pathnames<ReadonlyArray<'en' | 'de'>>
});
Expand All @@ -895,10 +899,12 @@ describe('prefix-based routing', () => {
middlewareWithPathnames(
createMockRequest('/en/news/happy-newyear-g5b116754', 'en')
);
middlewareWithPathnames(createMockRequest('/en/categories', 'en'));
middlewareWithPathnames(createMockRequest('/en/categories/new', 'en'));

expect(MockedNextResponse.redirect).not.toHaveBeenCalled();
expect(MockedNextResponse.next).not.toHaveBeenCalled();
expect(MockedNextResponse.rewrite).toHaveBeenCalledTimes(4);
expect(MockedNextResponse.rewrite).toHaveBeenCalledTimes(6);
expect(
MockedNextResponse.rewrite.mock.calls.map((call) =>
call[0].toString()
Expand All @@ -907,7 +913,9 @@ describe('prefix-based routing', () => {
'http://localhost:3000/en/about',
'http://localhost:3000/en/users',
'http://localhost:3000/en/users/1',
'http://localhost:3000/en/news/happy-newyear-g5b116754'
'http://localhost:3000/en/news/happy-newyear-g5b116754',
'http://localhost:3000/en/categories',
'http://localhost:3000/en/categories/new'
]);
});

Expand All @@ -928,21 +936,24 @@ describe('prefix-based routing', () => {
middlewareWithPathnames(
createMockRequest('/de/neuigkeiten/gutes-neues-jahr-g5b116754', 'de')
);
middlewareWithPathnames(createMockRequest('/de/kategorien', 'de'));
middlewareWithPathnames(createMockRequest('/de/kategorien/neu', 'de'));

expect(MockedNextResponse.next).not.toHaveBeenCalled();
expect(MockedNextResponse.redirect).not.toHaveBeenCalled();
expect(MockedNextResponse.rewrite.mock.calls[0][0].toString()).toBe(
'http://localhost:3000/de/about'
);
expect(MockedNextResponse.rewrite.mock.calls[1][0].toString()).toBe(
'http://localhost:3000/de/users'
);
expect(MockedNextResponse.rewrite.mock.calls[2][0].toString()).toBe(
'http://localhost:3000/de/users/1'
);
expect(MockedNextResponse.rewrite.mock.calls[3][0].toString()).toBe(
'http://localhost:3000/de/news/gutes-neues-jahr-g5b116754'
);

expect(
MockedNextResponse.rewrite.mock.calls.map((call) =>
call[0].toString()
)
).toEqual([
'http://localhost:3000/de/about',
'http://localhost:3000/de/users',
'http://localhost:3000/de/users/1',
'http://localhost:3000/de/news/gutes-neues-jahr-g5b116754',
'http://localhost:3000/de/categories',
'http://localhost:3000/de/categories/neu'
]);
});

it('redirects a request for a localized route that is not associated with the requested locale', () => {
Expand Down
24 changes: 24 additions & 0 deletions packages/next-intl/test/middleware/utils.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {describe, expect, it} from 'vitest';
import {
formatPathname,
getInternalTemplate,
getNormalizedPathname,
getRouteParams
} from '../../src/middleware/utils';
Expand Down Expand Up @@ -125,3 +126,26 @@ describe('formatPathname', () => {
);
});
});

describe('getInternalTemplate', () => {
const pathnames = {
'/categories/[[...slug]]': {
en: '/categories/[[...slug]]',
de: '/kategorien/[[...slug]]'
}
};

it('works when passing no params to optional catch-all segments', () => {
expect(getInternalTemplate(pathnames, '/kategorien')).toEqual([
'de',
'/categories/[[...slug]]'
]);
});

it('works when passing params to optional catch-all segments', () => {
expect(getInternalTemplate(pathnames, '/kategorien/neu')).toEqual([
'de',
'/categories/[[...slug]]'
]);
});
});
1 change: 1 addition & 0 deletions packages/next-intl/test/shared/utils.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe('matchesPathname', () => {
expect(matchesPathname('/[[...slug]]', '/products/clothing/t-shirts')).toBe(
true
);
expect(matchesPathname('/products/[[...slug]]', '/products')).toBe(true);
});

it('returns false for non-matching paths', () => {
Expand Down

0 comments on commit 8ba8b69

Please sign in to comment.