Skip to content

Commit

Permalink
feat: Add localePrefix for navigation APIs for an improved initial …
Browse files Browse the repository at this point in the history
…render of `Link` when using `localePrefix: never`. Also fix edge case in middleware when using localized pathnames for redirects that remove a locale prefix (fixes an infinite loop). (#678)

By accepting an optional `localePrefix` for the navigation APIs, we can
get the initial render of the `href` of `Link` right if `localePrefix: 'never'` is set. This can be helpful if domain-based routing is used and
you have a single locale per domain.

Note that this change is backward-compatible. It's now recommended to
set the `localePrefix` for the navigation APIs to get improved behavior
for `Link` in case `localePrefix: 'never'` is used, but otherwise your
app will keep working with the previous behavior.





Ref #444
  • Loading branch information
amannn authored Nov 29, 2023
1 parent ae4c2db commit 1c68e3b
Show file tree
Hide file tree
Showing 36 changed files with 1,368 additions and 736 deletions.
4 changes: 3 additions & 1 deletion docs/pages/docs/routing/middleware.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ The bestmatching domain is detected based on these priorities:

### Locale prefix

If you're using [the navigation APIs from `next-intl`](/docs/routing/navigation), you want to make sure your `localePrefix` setting matches your middleware configuration.

#### Always use a locale prefix [#locale-prefix-always]

By default, pathnames always start with the locale (e.g. `/en/about`).
Expand Down Expand Up @@ -200,7 +202,7 @@ In this case, requests for all locales will be rewritten to have the locale only

<Callout>
Note that [alternate links](#disable-alternate-links) are disabled in this
mode since there are no distinct URLs per language.
mode since there might not be distinct URLs per locale.
</Callout>

### Disable automatic locale detection
Expand Down
16 changes: 10 additions & 6 deletions docs/pages/docs/routing/navigation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,21 @@ To create [navigation APIs](#apis) for this strategy, use the `createSharedPathn
import {createSharedPathnamesNavigation} from 'next-intl/navigation';

export const locales = ['en', 'de'] as const;
export const localePrefix = 'always'; // Default

export const {Link, redirect, usePathname, useRouter} =
createSharedPathnamesNavigation({locales});
createSharedPathnamesNavigation({locales, localePrefix});
```

The `locales` argument is identical to the configuration that you pass to the middleware. To reuse it there, you can import the `locales` into the middleware.
The `locales` as well as the `localePrefix` argument is identical to the configuration that you pass to the middleware. You might want to share these values via a central configuration to keep them in sync.

```tsx filename="middleware.ts"
import createMiddleware from 'next-intl/middleware';
import {locales} from './navigation';
import {locales, localePrefix} from './navigation';

export default createMiddleware({
defaultLocale: 'en',
localePrefix,
locales
});
```
Expand All @@ -69,6 +71,7 @@ import {
} from 'next-intl/navigation';

export const locales = ['en', 'de'] as const;
export const localePrefix = 'always'; // Default

// The `pathnames` object holds pairs of internal
// and external paths, separated by locale.
Expand Down Expand Up @@ -99,17 +102,18 @@ export const pathnames = {
} satisfies Pathnames<typeof locales>;

export const {Link, redirect, usePathname, useRouter, getPathname} =
createLocalizedPathnamesNavigation({locales, pathnames});
createLocalizedPathnamesNavigation({locales, localePrefix, pathnames});
```

The `pathnames` argument is identical to the configuration that you pass to the middleware for [localizing pathnames](/docs/routing/middleware#localizing-pathnames). Because of this, you might want to import the `locales` and `pathnames` into the middleware.
The arguments `locales`, `localePrefix` as well as `pathnames` are identical to the configuration that you pass to the middleware. You might want to share these values via a central configuration to make sure they stay in sync.

```tsx filename="middleware.ts"
import createMiddleware from 'next-intl/middleware';
import {locales, pathnames} from './navigation';
import {locales, localePrefix, pathnames} from './navigation';

export default createMiddleware({
defaultLocale: 'en',
localePrefix,
locales,
pathnames
});
Expand Down
4 changes: 2 additions & 2 deletions examples/example-app-router-playground/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import createMiddleware from 'next-intl/middleware';
import {locales, pathnames} from './navigation';
import {locales, pathnames, localePrefix} from './navigation';

export default createMiddleware({
defaultLocale: 'en',
localePrefix: 'as-needed',
localePrefix,
pathnames,
locales
});
Expand Down
3 changes: 3 additions & 0 deletions examples/example-app-router-playground/src/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {

export const locales = ['en', 'de', 'es'] as const;

export const localePrefix = 'as-needed';

export const pathnames = {
'/': '/',
'/client': '/client',
Expand All @@ -25,5 +27,6 @@ export const pathnames = {
export const {Link, redirect, usePathname, useRouter} =
createLocalizedPathnamesNavigation({
locales,
localePrefix,
pathnames
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {useLocale, useTranslations} from 'next-intl';
import {locales} from 'config';
import {locales} from '../config';
import LocaleSwitcherSelect from './LocaleSwitcherSelect';

export default function LocaleSwitcher() {
Expand Down
3 changes: 3 additions & 0 deletions examples/example-app-router/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ export const pathnames = {
}
} satisfies Pathnames<typeof locales>;

// Use the default: `always`
export const localePrefix = undefined;

export type AppPathnames = keyof typeof pathnames;
5 changes: 3 additions & 2 deletions examples/example-app-router/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import createMiddleware from 'next-intl/middleware';
import {pathnames, locales} from './config';
import {pathnames, locales, localePrefix} from './config';

export default createMiddleware({
defaultLocale: 'en',
locales,
pathnames
pathnames,
localePrefix
});

export const config = {
Expand Down
5 changes: 3 additions & 2 deletions examples/example-app-router/src/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {createLocalizedPathnamesNavigation} from 'next-intl/navigation';
import {locales, pathnames} from './config';
import {locales, pathnames, localePrefix} from './config';

export const {Link, redirect, usePathname, useRouter} =
createLocalizedPathnamesNavigation({
locales,
pathnames
pathnames,
localePrefix
});
5 changes: 3 additions & 2 deletions packages/next-intl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"@types/negotiator": "^0.6.1",
"@types/node": "^17.0.23",
"@types/react": "18.2.34",
"@types/react-dom": "^18.2.17",
"eslint": "^8.54.0",
"eslint-config-molindo": "^7.0.0",
"eslint-plugin-deprecation": "^1.4.1",
Expand All @@ -110,11 +111,11 @@
},
{
"path": "dist/production/navigation.react-client.js",
"limit": "2.6 KB"
"limit": "2.62 KB"
},
{
"path": "dist/production/navigation.react-server.js",
"limit": "2.75 KB"
"limit": "2.8 KB"
},
{
"path": "dist/production/server.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import {AllLocales, Pathnames} from '../shared/types';

type LocalePrefix = 'as-needed' | 'always' | 'never';
import {AllLocales, LocalePrefix, Pathnames} from '../shared/types';

type RoutingBaseConfig<Locales extends AllLocales> = {
/** A list of all locales that are supported. */
Expand Down
15 changes: 9 additions & 6 deletions packages/next-intl/src/middleware/middleware.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,16 @@ export default function createMiddleware<Locales extends AllLocales>(
response = redirect(pathWithSearch);
}
} else {
const pathWithSearch = getPathWithSearch(
const internalPathWithSearch = getPathWithSearch(
pathname,
request.nextUrl.search
);

if (hasLocalePrefix) {
const basePath = getBasePath(pathWithSearch, pathLocale);
const basePath = getBasePath(
getPathWithSearch(normalizedPathname, request.nextUrl.search),
pathLocale
);

if (configWithDefaults.localePrefix === 'never') {
response = redirect(basePath);
Expand All @@ -205,10 +208,10 @@ export default function createMiddleware<Locales extends AllLocales>(
if (domain?.domain !== pathDomain?.domain && !hasUnknownHost) {
response = redirect(basePath, pathDomain?.domain);
} else {
response = rewrite(pathWithSearch);
response = rewrite(internalPathWithSearch);
}
} else {
response = rewrite(pathWithSearch);
response = rewrite(internalPathWithSearch);
}
}
} else {
Expand All @@ -221,9 +224,9 @@ export default function createMiddleware<Locales extends AllLocales>(
(configWithDefaults.localePrefix === 'as-needed' ||
configWithDefaults.domains))
) {
response = rewrite(`/${locale}${pathWithSearch}`);
response = rewrite(`/${locale}${internalPathWithSearch}`);
} else {
response = redirect(`/${locale}${pathWithSearch}`);
response = redirect(`/${locale}${internalPathWithSearch}`);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
import React, {ComponentProps, ReactElement, forwardRef} from 'react';
import useLocale from '../../react-client/useLocale';
import BaseLinkWithLocale from '../../shared/BaseLinkWithLocale';
import {AllLocales} from '../../shared/types';
import BaseLink from '../shared/BaseLink';

type Props<Locales extends AllLocales> = Omit<
ComponentProps<typeof BaseLinkWithLocale>,
ComponentProps<typeof BaseLink>,
'locale'
> & {
locale?: Locales[number];
};

function BaseLink<Locales extends AllLocales>(
function ClientLink<Locales extends AllLocales>(
{locale, ...rest}: Props<Locales>,
ref: Props<Locales>['ref']
) {
const defaultLocale = useLocale();
const linkLocale = locale || defaultLocale;
return (
<BaseLinkWithLocale
ref={ref}
hrefLang={linkLocale}
locale={linkLocale}
{...rest}
/>
<BaseLink ref={ref} hrefLang={linkLocale} locale={linkLocale} {...rest} />
);
}

Expand All @@ -46,8 +41,10 @@ function BaseLink<Locales extends AllLocales>(
* the `set-cookie` response header would cause the locale cookie on the current
* page to be overwritten before the user even decides to change the locale.
*/
const BaseLinkWithRef = forwardRef(BaseLink) as <Locales extends AllLocales>(
const ClientLinkWithRef = forwardRef(ClientLink) as <
Locales extends AllLocales
>(
props: Props<Locales> & {ref?: Props<Locales>['ref']}
) => ReactElement;
(BaseLinkWithRef as any).displayName = 'Link';
export default BaseLinkWithRef;
(ClientLinkWithRef as any).displayName = 'ClientLink';
export default ClientLinkWithRef;
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import useLocale from '../../react-client/useLocale';
import redirectWithLocale from '../../shared/redirectWithLocale';
import {ParametersExceptFirstTwo} from '../../shared/types';
import {LocalePrefix, ParametersExceptFirstTwo} from '../../shared/types';
import baseRedirect from '../shared/baseRedirect';

export default function baseRedirect(
pathname: string,
...args: ParametersExceptFirstTwo<typeof redirectWithLocale>
export default function clientRedirect(
params: {localePrefix?: LocalePrefix; pathname: string},
...args: ParametersExceptFirstTwo<typeof baseRedirect>
) {
let locale;
try {
Expand All @@ -18,5 +18,5 @@ export default function baseRedirect(
);
}

return redirectWithLocale(pathname, locale, ...args);
return baseRedirect({...params, locale}, ...args);
}
Loading

2 comments on commit 1c68e3b

@vercel
Copy link

@vercel vercel bot commented on 1c68e3b Nov 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 1c68e3b Nov 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-intl-docs – ./docs

next-intl-docs.vercel.app
next-intl-docs-git-main-next-intl.vercel.app
next-intl-docs-next-intl.vercel.app

Please sign in to comment.