-
-
Notifications
You must be signed in to change notification settings - Fork 247
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
feat: Automatically prefer localized pathnames that are more specific #983
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
@fkapsahili is attempting to deploy a commit to the next-intl Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Many thanks for looking into this and another great PR @fkapsahili! 👏👏
On a first glance, this looks like just the right implementation. I have a somewhat busy week ahead, but I'll likely be able to take a thorough look on Friday—hope that works for you!
I'll be in touch! 🙌
Thanks for the feedback! Yes of course, that works well for me 🙂. The tests are all working now, so I'm setting the PR to ready for review 😄. |
const dynamicA = isDynamicRoute(pathA); | ||
const dynamicB = isDynamicRoute(pathB); | ||
const catchAllA = isCatchAllRoute(pathA); | ||
const catchAllB = isCatchAllRoute(pathB); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Diving a bit deeper into this, I'm not sure these flags are sufficient to be honest.
E.g. consider this case:
/articles/[category]/[articleSlug]
/articles/[category]/just-in
In this case, the routes would be considered equal in regard to priority, and probably the natural ordering would be preserved. However, the second route is more specific and should match.
The number of dynamic segments could be another indicator, but I think this won't solve the problem cleanly as e.g. /foo/[d1]
and /foo/bar/[d1]
have the same number of segments.
It seems like to be able to tell which route is really more specific, we have to consider routes in a hierarchical way, e.g. find the most specific route in /articles
, then try to find the most specific route in ./[category]
, etc.
Do you by chance know if Next.js handles these cases correctly? Maybe we can take some inspiration from them.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or maybe the number of dynamic segments could in fact suffice? 🤔
In the example /foo/[d1]
and /foo/bar/[d1]
, the routes have the same priority, but only one of them matches eventually—so maybe this isn't an issue.
Do you think you could a few more tests for edge cases so we can gain some clarity about those?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, thanks for the hint! You were absolutely right, my solution didn't take into account the case where we have nested dynamic routes, e.g: /articles/[category]/[slug]
and a static route in the same place 😅.
According to my tests, Next.js handles these cases correctly but I'm not very familiar with the Next.js repo itself, but at first glance I believe the code you referenced in the corresponding issue is actually where Next does the route sorting:
https://github.com/vercel/next.js/blob/415cd74b9a220b6f50da64da68c13043e9b02995/packages/next/src/shared/lib/router/utils/sorted-routes.ts
When looking at Next's code I realized that it is necessary that the individual route segments are compared with each other and have now implemented this additionally so that the failed tests that I added at the beginning of this update now pass.
If there are any other edge cases you can think of, I'm happy to add more tests! 🙂
…//github.com/fkapsahili/next-intl into fix/automatically-prefer-localized-pathnames
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great refactor, this looks really solid to me and I think this is the right approach 👏.
I've left some inline comments about minor nits, but I also think there might be a chance to simplify this function slightly.
Let me know what you think! 🙌
const pathA = getExternalPath(a[1]).split('/').filter(Boolean); | ||
const pathB = getExternalPath(b[1]).split('/').filter(Boolean); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, what is your intention with getExternalPath
here? I don't really understand why we need to extract firstLocalePath
. It seems a bit odd that we take the first localized pathname. Why don't we just use the internal route name?
Maybe to take a step back: I would assume that users use localized pathnames to localize individual segments, but the number and also the name of dynamic segments should always match. Maybe some validation could help in regard to that to avoid typos (e.g. '/[category]': '/[categry]'
, but generally, in the context of sorting routes, we should only have to consider the internal pathname I think.
Furthermore, I'm not sure we need to run .filter
—did you have a specific case for this in mind?
Considering these two points, I think we could simplify to this:
const pathA = getExternalPath(a[1]).split('/').filter(Boolean); | |
const pathB = getExternalPath(b[1]).split('/').filter(Boolean); | |
const pathA = a[0].split('/'); | |
const pathB = b[0].split('/'); |
Based on this, if we only consider the internal pathname, maybe we could simplify the sort function a bit:
expect(
getSortedPathnames(
['/categories/[slug]', '/categories/new']
)
).toEqual(
['/categories/new', '/categories/[slug]']
);
Might make the tests a bit more minimalistic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, that makes sense! I think the getExternalPath
comes from a first attempt where I tried to separate everything very strictly. Just taking the internal route name is a good idea! I'll try to clean that up a bit so we have an easier to read test suite and a more minimalistic approach in the utils 👍🏼.
Co-authored-by: Jan Amann <jan@amann.work>
Co-authored-by: Jan Amann <jan@amann.work>
Co-authored-by: Jan Amann <jan@amann.work>
Co-authored-by: Jan Amann <jan@amann.work>
Co-authored-by: Jan Amann <jan@amann.work>
Co-authored-by: Jan Amann <jan@amann.work>
Co-authored-by: Jan Amann <jan@amann.work>
@amannn I have now simplified the sorting function of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks fantastic, a really cool quality-of-life improvement! 👏
I'll just add one commit with a minor renaming nit I spotted during the final review.
…sDynamicRoute` was used from a previous iteration of the feature)
This PR fixes the current behavior of the internal route handling of the
next-intl
middleware that does not prioritize static route paths over dynamic routes. This fix makes sure that static routes (e.g./products/new
) are preferred over dynamic routes such as/products/[slug]
.To make sure the current behavior does not lead to any unexpected issues, I thought it would be good if
next-intl
would prioritize the specifiedpathnames
in the following order:/products/new
/products/[slug]
/products/[...slug]
or optional catch-all routes, e.g./products/[[...slug]]
Prerequisites
Content
getSortedPathnames
to handle the sorting of the specified pathnames in themiddleware
docs
to make sure the changes are reflected in the documentationTests
middleware
that verify that static routes are prioritized over dynamic routes and catch-all routesutils
to make sure thatgetSortedPathnames
handles the route prioritization as intendedexample-app-router-playground
to make sure the route prioritization works as intendedCloses #913