Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Automatically prefer localized pathnames that are more specific #983

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2550e48
Ensure more specific localized pathnames are preferred
fkapsahili Apr 6, 2024
8fa7d7e
Make sure that static pathnames are prioritized over optional catch-a…
fkapsahili Apr 6, 2024
012de6c
Update docs to reflect changes in route prioritization
fkapsahili Apr 6, 2024
1837b98
Increase middleware size limit
fkapsahili Apr 6, 2024
752f01e
Add playwright tests
fkapsahili Apr 6, 2024
3d4d3a6
Update middleware docs
fkapsahili Apr 8, 2024
cd438a9
Make sure prioritization works for nested dynamic routes
fkapsahili Apr 13, 2024
8dd2573
Merge branch 'main' into fix/automatically-prefer-localized-pathnames
fkapsahili Apr 13, 2024
40c994a
Increase middleware size limit
fkapsahili Apr 13, 2024
bc659c9
Merge branch 'fix/automatically-prefer-localized-pathnames' of https:…
fkapsahili Apr 13, 2024
f7a8bfb
Increase middleware size limit
fkapsahili Apr 13, 2024
d13fcf3
Merge branch 'main' into fix/automatically-prefer-localized-pathnames
fkapsahili Apr 17, 2024
0efe8a2
Increase middleware size limit
fkapsahili Apr 17, 2024
1f00897
Update packages/next-intl/src/middleware/utils.tsx
fkapsahili Apr 22, 2024
effc0e6
Update packages/next-intl/src/middleware/utils.tsx
fkapsahili Apr 22, 2024
961c61f
Update packages/next-intl/test/middleware/middleware.test.tsx
fkapsahili Apr 22, 2024
9e9fd75
Update packages/next-intl/test/middleware/middleware.test.tsx
fkapsahili Apr 22, 2024
d983757
Update packages/next-intl/test/middleware/middleware.test.tsx
fkapsahili Apr 22, 2024
2708b17
Update packages/next-intl/test/middleware/middleware.test.tsx
fkapsahili Apr 22, 2024
686882a
Merge branch 'main' into fix/automatically-prefer-localized-pathnames
fkapsahili Apr 22, 2024
bad1728
Update packages/next-intl/test/middleware/utils.test.tsx
fkapsahili Apr 22, 2024
f73628a
Fix misspellings
fkapsahili Apr 22, 2024
3f3acb4
Simplify pathname sorting function
fkapsahili Apr 22, 2024
59561c2
Fix misspellings in playwright tests
fkapsahili Apr 23, 2024
635a4bf
Rename new functions to indicate we're operating on segments (e.g. `i…
amannn Apr 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 7 additions & 8 deletions docs/pages/docs/routing/middleware.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -321,20 +321,19 @@ export default createMiddleware({
de: '/ueber-uns'
},

// Pathnames that overlap with dynamic segments should
// be listed first in the object, as the middleware will
// pick the first matching entry.
'/news/just-in': {
en: '/news/just-in',
de: '/neuigkeiten/aktuell'
},

// Dynamic params are supported via square brackets
'/news/[articleSlug]-[articleId]': {
en: '/news/[articleSlug]-[articleId]',
de: '/neuigkeiten/[articleSlug]-[articleId]'
},

// Static pathnames that overlap with dynamic segments
// will be prioritized over the dynamic segment
'/news/just-in': {
en: '/news/just-in',
de: '/neuigkeiten/aktuell'
},

// Also (optional) catch-all segments are supported
'/categories/[...slug]': {
en: '/categories/[...slug]',
Expand Down
3 changes: 3 additions & 0 deletions examples/example-app-router-playground/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"NewsArticle": {
"title": "News-Artikel #{articleId}"
},
"JustIn": {
"title": "Gerade eingetroffen"
},
"NotFound": {
"title": "Diese Seite wurde nicht gefunden (404)"
},
Expand Down
3 changes: 3 additions & 0 deletions examples/example-app-router-playground/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"NewsArticle": {
"title": "News article #{articleId}"
},
"JustIn": {
"title": "Just in"
},
"NotFound": {
"title": "This page was not found (404)"
},
Expand Down
3 changes: 3 additions & 0 deletions examples/example-app-router-playground/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"NewsArticle": {
"title": "Noticias #{articleId}"
},
"JustIn": {
"title": "Recién llegado"
},
"NotFound": {
"title": "Esta página no se encontró (404)"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {useTranslations} from 'next-intl';

export default function NewsArticle() {
const t = useTranslations('JustIn');
return <h1>{t('title')}</h1>;
}
6 changes: 6 additions & 0 deletions examples/example-app-router-playground/src/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export const pathnames = {
de: '/neuigkeiten/[articleId]',
es: '/noticias/[articleId]',
ja: '/ニュース/[articleId]'
},
'/news/just-in': {
en: '/news/just-in',
de: '/neuigkeiten/aktuell',
es: '/noticias/justo-en',
ja: '/ニュース/現在'
}
} satisfies Pathnames<typeof locales>;

Expand Down
18 changes: 18 additions & 0 deletions examples/example-app-router-playground/tests/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,24 @@ it('redirects unprefixed paths for non-default locales', async ({browser}) => {
page.getByRole('heading', {name: 'Verschachtelt'});
});

it('prioritizes static routes over dynamic routes for the default locale', async ({
page
}) => {
await page.goto('/news/just-in');
await expect(page).toHaveURL('/news/just-in');
await expect(page.getByRole('heading', {name: 'Just In'})).toBeVisible();
});

it('prioritizes static routes over dynamic routes for non-default locales', async ({
page
}) => {
await page.goto('/de/neuigkeiten/aktuell');
await expect(page).toHaveURL('/de/neuigkeiten/aktuell');
await expect(
page.getByRole('heading', {name: 'Gerade eingetroffen'})
).toBeVisible();
});

it('sets the `path` for the cookie', async ({page}) => {
await page.goto('/de/client');

Expand Down
2 changes: 1 addition & 1 deletion packages/next-intl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@
},
{
"path": "dist/production/middleware.js",
"limit": "6 KB"
"limit": "6.19 KB"
}
]
}
66 changes: 63 additions & 3 deletions packages/next-intl/src/middleware/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,65 @@ export function getFirstPathnameSegment(pathname: string) {
return pathname.split('/')[1];
}

function isOptionalCatchAllSegment(pathname: string) {
return pathname.includes('[[...');
}

function isCatchAllSegment(pathname: string) {
return pathname.includes('[...');
}

function isDynamicSegment(pathname: string) {
return pathname.includes('[');
}

export function comparePathnamePairs(a: string, b: string): number {
const pathA = a.split('/');
const pathB = b.split('/');

const maxLength = Math.max(pathA.length, pathB.length);
for (let i = 0; i < maxLength; i++) {
fkapsahili marked this conversation as resolved.
Show resolved Hide resolved
const segmentA = pathA[i];
const segmentB = pathB[i];

// If one of the paths ends, prioritize the shorter path
if (!segmentA && segmentB) return -1;
if (segmentA && !segmentB) return 1;

// Prioritize static segments over dynamic segments
if (!isDynamicSegment(segmentA) && isDynamicSegment(segmentB)) return -1;
if (isDynamicSegment(segmentA) && !isDynamicSegment(segmentB)) return 1;

// Prioritize non-catch-all segments over catch-all segments
if (!isCatchAllSegment(segmentA) && isCatchAllSegment(segmentB)) return -1;
if (isCatchAllSegment(segmentA) && !isCatchAllSegment(segmentB)) return 1;

// Prioritize non-optional catch-all segments over optional catch-all segments
if (
!isOptionalCatchAllSegment(segmentA) &&
isOptionalCatchAllSegment(segmentB)
) {
return -1;
}
if (
isOptionalCatchAllSegment(segmentA) &&
!isOptionalCatchAllSegment(segmentB)
) {
return 1;
}

if (segmentA === segmentB) continue;
fkapsahili marked this conversation as resolved.
Show resolved Hide resolved
}

// Both pathnames are completely static
return 0;
}

export function getSortedPathnames(pathnames: Array<string>) {
const sortedPathnames = pathnames.sort(comparePathnamePairs);
return sortedPathnames;
}

export function getInternalTemplate<
Locales extends AllLocales,
Pathnames extends NonNullable<
Expand All @@ -19,10 +78,11 @@ export function getInternalTemplate<
pathname: string,
locale: Locales[number]
): [Locales[number] | undefined, keyof Pathnames | undefined] {
const sortedPathnames = getSortedPathnames(Object.keys(pathnames));

// Try to find a localized pathname that matches
for (const [internalPathname, localizedPathnamesOrPathname] of Object.entries(
pathnames
)) {
for (const internalPathname of sortedPathnames) {
const localizedPathnamesOrPathname = pathnames[internalPathname];
if (typeof localizedPathnamesOrPathname === 'string') {
const localizedPathname = localizedPathnamesOrPathname;
if (matchesPathname(localizedPathname, pathname)) {
Expand Down